APIM JSON Schema Validation done easy
Tags: azure, apim, bicep, json
Published: 2022-06-16 13:16:22

For way too long we had to use transformation policy (`<set-body>`, `throw new Exception`, `<on-error>`) to validate JSON schema. 

No more =)
We finally have [content validation policies](https://docs.microsoft.com/en-us/azure/api-management/validation-policies#validate-content):
> The policy validates the following content in the request or response against the schema:
- Presence of all required properties.
- Absence of additional properties, if the schema has the additionalProperties field set to false.
- Types of all properties. For example, if a schema specifies a property as an integer, the request (or response) must include an integer and not another type, such as a string.
- The format of the properties, if specified in the schema - for example, regex (if the pattern keyword is specified), minimum for integers, and so on.

And by "finally" I mean, since a few months now. But I don't see many posts about it, so here we are =)

Let's assume I already have an [APIM bicep template](https://github.com/Azure/bicep/tree/main/docs/examples/101/api-management-modular) that provisions API Management Instance.

I will now extend my Bicep template with schema to be used in validation, and in definitions.

## Defining JSON schema
First things first. Below is my schema and payload example.

#### main.bicep 
```bicep
var schemaExampleUser1 = 'john.doe@${tenantName}.onmicrosoft.com'

var schemaPersonRequired = [
  'firstName'
  'lastName'
]
var schemaPerson = {
  firstName: {
    type: 'string'
  }
  lastName: {
    type: 'string'
  }
  age: {
    type: 'integer'
    minimum: 0
  }
  email: {
    type: 'string'
    format: 'email'
    pattern: '^\\S+@\\S+\\.\\S+$'
  }
}
var personExample = {
  firstName: 'John'
  lastName: 'Doe'
  age: 25
  email: schemaExampleUser1
}
```

## API Management resources

### Policies 
I have a number of policies that I want to mix-and-match depending on the API. To keep things neat, I keep them in separate variables, and compose a final definition as needed.

#### main.bicep 
```bicep
var authorizationPolicy = '''
        <!-- Service Bus Authorization-->
        <authentication-managed-identity resource="https://servicebus.azure.net/" output-token-variable-name="msi-access-token" ignore-error="false" />
        <set-header name="Authorization" exists-action="override">
            <value>@("Bearer " + (string)context.Variables["msi-access-token"])</value>
        </set-header>
        <set-header name="Content-Type" exists-action="override">
            <value>application/atom+xml;type=entry;charset=utf-8</value>
        </set-header>
        <set-header name="BrokerProperties" exists-action="override">
            <value>{}</value>
        </set-header>
'''

var mockResponse = '<mock-response status-code="200" content-type="application/json" />'

var validatePersonPolicy = '''
        <validate-content unspecified-content-type-action="detect" max-size="102400" size-exceeded-action="prevent" errors-variable-name="validationErrors">
          <content type="application/json" validate-as="json" action="prevent" schema-id="Person" />
        </validate-content>
'''

var policySchema = '''
  <!--ADD {0}-->
  <policies>
      <inbound>
        <base />
        <!-- Validation -->
        {1}
        <!-- Authorization -->
        {2}
        <!-- Mock response -->
        {3}
      </inbound>
      <backend>
        <base />
      </backend>
      <outbound>
        <base />
      </outbound>
      <on-error>
        <base />
      </on-error>
    </policies>
'''

var personPolicy = format(policySchema, 'PORTFOLIO', validatePersonPolicy, authorizationPolicy, '<!-- N/A -->')
```


### Schema
Now it's time to create [schema](https://docs.microsoft.com/en-us/azure/templates/microsoft.apimanagement/service/schemas?tabs=bicep) definition in the already existing API Management instance. 

#### main.bicep 
```bicep
resource apiManagement 'Microsoft.ApiManagement/service@2021-08-01' existing = {
  name: serviceName
}

resource apiManagement_schemaPerson 'Microsoft.ApiManagement/service/schemas@2021-08-01' = {
  name: 'Person'
  parent: apiManagement
  properties: {
    schemaType: 'json'
    description: 'Schema for a Person Object'
    document: any({
      type: 'array'
      items: {
        type: 'object'
        properties: schemaPerson
        required: schemaPersonRequired
      }
    })
  }
}
```
After deployment, you will find it under **APIs/Schemas**:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9luh6m1m2p0hjwqxico7.png)
 
### API 
Next step is to create an API, definition, and an operation. I'm  referencing the schema and the example created in the first step.
In this example, the `Add Person` operation saves the payload to a ServiceBus queue, that's why the `urlTemplate` is set to `/${serviceBusQueueName1}/messages`. 
_I'm mentioning it just to point out you have to remember about the "messages" at the end of the url_ ;)

#### main.bicep 
```bicep
resource apiManagement_apiName 'Microsoft.ApiManagement/service/apis@2021-08-01' = {
  name: '${serviceName}/${apiName}'
  properties: {
    displayName: apiDisplayName
    subscriptionRequired: true
    path: 'person-schema-validation'
    protocols: [
      'https'
    ]
    isCurrent: true
    description: 'Personal data ingestion'
    subscriptionKeyParameterNames: {
      header: 'Subscription-Key-Header-Name'
      query: 'subscription-key-query-param-name'
    }
  }
}

resource apiManagement_apiName_apiSchemaGuid 'Microsoft.ApiManagement/service/apis/schemas@2021-08-01' = {
  parent: apiManagement_apiName
  name: apiSchemaGuid
  properties: {
    contentType: 'application/vnd.oai.openapi.components+json'
    document: any({
      components: {
        schemas: {
          Definition_Person: {
            type: 'object'
            properties: schemaPerson
            required: schemaPersonRequired
            example: personExample
          }
        }
      }
    })
  }
}
resource apiManagement_apiName_operation_addPerson 'Microsoft.ApiManagement/service/apis/operations@2021-08-01' = {
  parent: apiManagement_apiName
  name: operation_addPerson

  dependsOn: [
    apiManagement_apiName_apiSchemaGuid
  ]
  properties: {
    request: {
      headers: [
        {
          name: 'Content-Type'
          type: 'string'
          required: true
          values: [
            'application/json'
          ]
        }
      ]
      representations: [
        {
          contentType: 'application/json'
          schemaId: apiSchemaGuid
          typeName: 'Definition_Person'
        }
      ]
    }
    displayName: 'Add Person'
    description: 'Add Person Information to ServiceBus. \nThe Request Body is parsed to ensure correct schema.'
    method: 'POST'
    urlTemplate: '/${serviceBusQueueName1}/messages'
  }
}

```

### Schema Validation

And now, that I have everything in place, I can use schema validation. Finally!

The first policy is actually not a validation. It is applied to all the operations, and it ensures that the backend url is set to the ServiceBus Endpoint. 

The second policy ensures that the payload receives passes schema validation.

#### main.bicep 
```bicep
resource serviceName_apiName_policy 'Microsoft.ApiManagement/service/apis/policies@2021-08-01' = {
  parent: apiManagement_apiName
  name: 'policy'
  properties: {
    value: '<!-- All operations-->\r\n<policies>\r\n  <inbound>\r\n    <base/>\r\n    <set-backend-service base-url="${serviceBusEndpoint}" />\r\n  <set-header name="Content-Type" exists-action="override">\r\n  <value>application/json</value>\r\n  </set-header>\r\n  </inbound>\r\n  <backend>\r\n    <base />\r\n  </backend>\r\n  <outbound>\r\n    <base />\r\n  </outbound>\r\n  <on-error>\r\n    <base />\r\n  </on-error>\r\n</policies>'
    format: 'rawxml'
  }
}
resource apiManagement_apiName_operation_addPerson_policy 'Microsoft.ApiManagement/service/apis/operations/policies@2021-08-01' = {
  parent: apiManagement_apiName_operation_addPerson
  name: 'policy'
  properties: {
    value: personPolicy
    format: 'rawxml'
  }
}
```
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d2zto0717a7aqkpggxe8.png)
 

The bicep template is on [GitHub](https://github.com/kkazala/bicepTemplates/tree/main/apim-validation-policy-json).