JSON schema validation for array that can have items with different keys

Viewed 956

I am trying to find a schema that will validate a given array having multiple items. The items can have 2 possible values for a key. But all the items should have the same value as the key.

If the 2 possible values are 'primary' and 'secondary', then all the keys should be 'primary' or all the keys should be 'secondary'. oneOf does not seem to be working in this case.

Is there a solution to this? Any help is appreciated. Thank you.

Schema:

{
  type: "object",
  properties: {
    values: {
      type: "array",
      uniqueItems: true,
      minItems: 1,
      maxItems: 100,
      items: { 
        anyOf: [  
          { $ref: "#/definitions/primaryObj"} ,
          { $ref: "#/definitions/secondaryObj"} 
        ]
      }
    },
  },
  definitions: {
    primaryObj: {
      type: "object",
      required: ["id", "primary"],
      properties: {
        id: {
          type: "string",
          description: "The id",
        },
        primary: {
          type: "string",
          description: "primary value",
        },
      },
    },
    secondaryObj: {
      type: "object",
      required: ["id", "secondary"],
      properties: {
        id: {
          type: "string",
          description: "The id",
        },
        secondary: {
          type: "string",
          description: "secondary value",
        },
      },
    },
  },
  required: ["values"],
}

Sample Input -

Input 1 - should PASS validation

{
    "values": [
        {
            "id": "1",
            "primary" : "hello"
        },
        {
            "id": "2",
            "primary" : "world"
        }
    ]
}

Input 2 - should PASS validation

{
    "values": [
        {
            "id": "1",
            "secondary" : "hello"
        },
        {
            "id": "2",
            "secondary" : "world"
        }
    ]
}

Input 3 - should FAIL validation

{
    "values": [
        {
            "id": "1",
            "primary" : "hello"
        },
        {
            "id": "2",
            "secondary" : "world"
        }
    ]
}
1 Answers

You were pretty close here. There are two changes you need to make in order to get the validation you want. (I'm going to be assuming you're using draft-07, although this applies to newer drafts also)

First, let's take the top section.

The anyOf keyword is specified as follows:

An instance validates successfully against this keyword if it
validates successfully against at least one schema defined by this
keyword's value.

https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-01#section-6.7.2

You only want ONE of the referenced subschemas to be true!

oneOf is defined similar:

An instance validates successfully against this keyword if it
validates successfully against exactly one schema defined by this
keyword's value.

https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-01#section-6.7.3

So we change your schema to check that only ONE of the references is valid...

      "items": {
        "oneOf": [
          {
            "$ref": "#/definitions/primaryObj"
          },
          {
            "$ref": "#/definitions/secondaryObj"
          }
        ]
      }

But this still is incorrect. Let's refresh what items does.

This keyword determines how child instances validate for arrays, and does not directly validate the immediate instance itself.

If "items" is a schema, validation succeeds if all elements in the array successfully validate against that schema.

https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-01#section-6.4.1

It LOOKS like we got this right, however the first paragraph in the quote above attempts to convey that items applies its subschema value to each item in the array, and not "as a whole array".

What our above subschema is doing, is checking each item in the array by itself, in isolation of the other items, that they are "primary" or "secondary" as you define.

What we WANT to do, is check that ALL items in the array are either "primary" or "secondary". To achive this, we need to move the oneOf outside items.

      "oneOf": [
        {
          "items": {
            "$ref": "#/definitions/primaryObj"
          }
        },
        {
          "items": {
            "$ref": "#/definitions/secondaryObj"
          }
        }
      ]

Almost there! This almost works, but we still find mixing primary and secondary doesn't cause validation to fail.

Let's check our assumptions. We assume that validation should fail when the instance data has primary and secondary in objects in the array. We can test this by changing one of the subschemas in our oneOf to false, forcing the first subschema definition (primary) to be checked. It should check that all items in the array are primary, and any secondary should cause validation failure.

We have to remember, JSON Schema is constraints based. Anything that isn't constrained, is allowed.

If we look at the definition for primaryObj, it requires and defines the validation for id and primary, but this doesn't inherintly prevent additioanl keys in the object. To do that, we need to add "additionalProperties": false` (to both definitions).

The end result looks like this. You can check out the live demo at https://jsonschema.dev/s/3ZKBp

{
  "$schema": "http://json-schema.org/draft-07/schema",
  "type": "object",
  "properties": {
    "values": {
      "type": "array",
      "uniqueItems": true,
      "minItems": 1,
      "maxItems": 100,
      "oneOf": [
        {
          "items": {
            "$ref": "#/definitions/primaryObj"
          }
        },
        {
          "items": {
            "$ref": "#/definitions/secondaryObj"
          }
        }
      ]
    }
  },
  "definitions": {
    "primaryObj": {
      "type": "object",
      "required": [
        "id",
        "primary"
      ],
      "properties": {
        "id": {
          "type": "string",
          "description": "The id"
        },
        "primary": {
          "type": "string",
          "description": "primary value"
        }
      },
      "additionalProperties": false
    },
    "secondaryObj": {
      "type": "object",
      "required": [
        "id",
        "secondary"
      ],
      "properties": {
        "id": {
          "type": "string",
          "description": "The id"
        },
        "secondary": {
          "type": "string",
          "description": "secondary value"
        }
      },
      "additionalProperties": false
    }
  },
  "required": [
    "values"
  ]
}
Related