Search code examples
jsonjsonschema

Json schema add property on if condition


In JSON schema, I want to add a validation that if the payload matches below

{
   "key1": "value1", 
   "key1": "value2"
}

Then expect a third key "key3" with any value.

But if the value of key1 and key2 doesn't match value1, value2 respectively then key3 should not be present.

JSON schema looks like this

{
  "$async": true,
  "additionalProperties": false,
  "properties": {
    "key1": {
      "type": "string"
    }, 
    "key2": {
      "type": "string"
    }
  },
  "required": ["key1"],
  "allOf": [
    {
      "if": {
        "properties": {
          "key1": {
            "const": "value1"
          }, 
          "key2": {
            "const": "value2"
          }
        }
      },
      "then": {
        "properties": {
          "key3": {
            "type": "string"
          }
        },
        "required": ["key3"]
      },
      "else": {
      }
    }
  ]
}

Valid inputs are

{
   "key1": "value1", 
   "key2": "value2", 
   "key3": "some-value"
}
--------
{
    "key1": "value1", 
    "key2": "other-value"
}
---------
{
    "key1": "value1"
}
---------
{
    "key1": "other-value", 
    "key2": "value2"
}
---------
{
    "key1": "other-value1", 
    "key2": "other-value2"
}

Invalid inputs are

{
   "key1": "value1", 
   "key2": "value2".   // key3 should be present
}
--------
{
   "key1": "hello", 
   "key2": "world",  
   "unexpected-key": "value"   // Any other key should not be allowed 
}
--------
{
    "key1": "value1", 
    "key2": "other-value", 
    "key3": "abc"    // should not be present
 
}
---------
{
    "key1": "other-value", 
    "key2": "value2",
    "key3": "abc"    // should not be present
}
---------
{
    "key1": "other-value1", 
    "key2": "other-value2",
    "key3": "abc"    // should not be present
}

For below payload, JSON schema validator

{
  "key1": "value1", 
  "key2": "value2", 
  "key3": "abc"
}

Throws below error

Property 'key3' has not been defined and the schema does not allow additional properties.

How do I achieve this conditional property?


Solution

  • The solution depends on which version (or "draft") you're able to use of JSON Schema. Draft 2019-09 or above provides a cleaner solution, but it's still possible in draft-07.

    (It's also possible before if/then/else was introduced using the implication method, but I won't go into that today.

    Best solution using 2019-09 or 2020-12.

    The unevaluatedProperties keyword can "see through" applicator keywords, like allOf, if and else.

    Unlike draft-07's additionalProperties, unevaluatedProperties is run after all other applicators in the schema schema object, and can know about successfully validated property values defined in deeper nested applicators.

    {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "unevaluatedProperties": false,
      "properties": {
        "key1": {
          "type": "string"
        },
        "key2": {
          "type": "string"
        }
      },
      "required": [
        "key1"
      ],
      "allOf": [
        {
          "if": {
            "required": ["key2"],
            "properties": {
              "key1": {
                "const": "value1"
              },
              "key2": {
                "const": "value2"
              }
            }
          },
          "then": {
            "required": ["key3"],
            "properties": {
              "key3": {
                "type": "string"
              }
            }
          }
        }
      ]
    }
    

    For testing draft 2019-09 and above, I recomend using https://json-schema.hyperjump.io playground

    (https://jsonschema.dev doesn't yet support draft 2019-09 and above).

    Solution using draft-07.

    For additionalProperties: false to work, it needs to have the properties defined in the same schema object in properties (or match patternProperties).

    Given in the first instance, you don't care about the specific value, the value in properties object is just true. The value validation is handled later. You could move it to the top level if you feel it's cleaner.

    With this change, our if/then worked from your example, apart from it didn't prevent key3 when it should have. We fix that by making use of not inside else.

    {
      "$schema": "http://json-schema.org/draft-07/schema",
      "additionalProperties": false,
      "properties": {
        "key1": {
          "type": "string"
        }, 
        "key2": {
          "type": "string"
        },
        "key3": true
      },
      "required": ["key1"],
      "allOf": [
        {
          "if": {
            "required": ["key2"],
            "properties": {
              "key1": {
                "const": "value1"
              }, 
              "key2": {
                "const": "value2"
              }
            }
          },
          "then": {
            "required": ["key3"],
            "properties": {
              "key3": {
                "type": "string"
              }
            }
          },
          "else": {
            "not": {
              "required": ["key3"]
            }
          }
        }
      ]
    }
    

    Demo: https://jsonschema.dev/s/ZTi4X