DEV Community

Kinga
Kinga

Posted on • Updated on

APIM JSON Schema Validation done easy

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:

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 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

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
}
Enter fullscreen mode Exit fullscreen mode

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

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 -->')
Enter fullscreen mode Exit fullscreen mode

Schema

Now it's time to create schema definition in the already existing API Management instance.

main.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
      }
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

After deployment, you will find it under APIs/Schemas:
Image description

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

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'
  }
}

Enter fullscreen mode Exit fullscreen mode

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

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'
  }
}
Enter fullscreen mode Exit fullscreen mode

Image description

The bicep template is on GitHub.

Top comments (2)

Collapse
 
mattfrear profile image
Matt Frear

Your validate-content defines schema-id="Portfolio" but I can't see "Portfolio" defined anywhere else, so this doesn't seem correct?

Collapse
 
kkazala profile image
Kinga

thank you @mattfrear for letting me know. 🙏 It should indeed be "Person" and not "Portfolio".
I have another schema used productively and I missed the schema-id parameter when preparing the sample