Search code examples
jsonjsonschemaajv

How to define a JSON schema that has variable required properties


I would like to know if I can define a JSON schema (draft 2019-09) that requires at least one of many properties possible for an object, and some times multiple of those properties. I already know of allOf, anyOf, oneOf, and dependantrequried but just can't figure out how to use them in the way I want.

I have an object with four keys:

  • amount
  • productId
  • invoiceId
  • jwt

I have the follow requirements:

  • amount, productId, and invoiceId can not be in the object together
  • invoiceId must have jwt
  • jwt may be alone or with amount or product
  • must have at least one of the four keys in the object will respecting the above rules.

I have tried this schema, this works in most cases excpet when jwt is alone:

{
  "type": "object",
  "properties": {
    "amount": {"type": "number"},
    "productId": {"type": "string"},
    "invoiceId": {"type": "string"},
    "jwt": {"type": "string"},
  },
  "dependantRequried": {
    "invoiceId": ["jwt"]
  },
  "oneOf": [
    {"required": ["amount"]},
    {"required": ["productId"]},
    {"required": ["invoiceId"]},
  ]
}

Adding {"required": ["jwt"]} to "oneOf" array means that it fails when invoiceId and jwt are together.

expected results:

json isValid
{"amount": 1} true
{"amount": 1, "jwt": "jwt-test"} true
{"productId": "prod-1"} true
{"productId": "prod-1", "jwt": "jwt-test"} true
{"invoiceId": "inv-1", "jwt": "jwt-test"} true
{"jwt": "jwt-test"} true
{"invoiceId": "inv-1"} false
{"amount": 1, "productId": "prod-1"} false
{"amount": 1, "productId": "prod-1", "jwt": "jwt-test"} false

Solution

  • You have a couple options.

    1. if/then/else

    Instead of using required directly, try an if/then construct to forbid the other properties when one is present.

    {
      // ...,
      "anyOf": [
        {
          "if": {"required": ["amount"]},
          "then": {
            "properties": {
              "productId": false,
              "invoiceId": false,
            }
          },
          "else": false
        },
        // repeat for productId and invoiceId
        {
          "properties": {
              "amount": false,
              "productId": false,
              "invoiceId": false
          }
        }
      ]
    }
    

    The first subschema says that if amount is present in the instance, the other two cannot have values, which is a roundabout way of saying they're disallowed. Repeat this for the other two properties.

    The last one covers the case where none of them are present.

    2. Just use properties

    This basically does the same thing, but some may consider it less readable. It's a style question, really.

    {
      // ...,
      "anyOf": [
        {
          "properties": {
              "amount": true,
              "productId": false,
              "invoiceId": false
          }
        },
        // repeat for productId and invoiceId
        {
          "properties": {
              "amount": false,
              "productId": false,
              "invoiceId": false
          }
        }
      ]
    }
    

    Here the first subschema allows only the amount property. Again, repeat this for the other two properties, and have the same last case.


    You also want to use anyOf instead of oneOf. oneOf would work, but it's a more intensive check. Some implementations can shortcut and not evaluate the other subschemas once they've found one that works. For a oneOf, though, all subschemas must always be checked. Since we've arranged this so that only one of these can be valid, anyOf will do.

    Lastly, you also have a typo: dependantRequried -> dependentRequired.

    You can test these out at https://json-everything.net/json-schema.