Search code examples
jsonschemajson-schema-validator

JsonSchema that can accept legacy and expanded payload


I am expanding an existing feature, and as such the API payload needs to be updated. Due to constraints outside of my control, I need to support the legacy payload in conjunction with the new payload. This validation also needs to be reimplemented as a JsonSchema per a new policy.

The legacy payload is set in stone, the new payload could possibly be tweaked with some pain, but I would prefer to keep it as-is.

The legacy payload is:

{
  "OPTION": {
    "property1": "someString",
    "property2": <integer>
  }
}

The new payload will add on additional properties as well as the ability to create multiple objects, e.g:

{
  "OPTION": {
    "object1": {
      "property1": "someString",
      "property2": <integer>,
      "property3": <boolean>
    },
    ...
    "objectN": {
      "property1": "someString",
      "property2": <integer>,
      "property3": <boolean>
    }
  }
}

My initial attempts have been unsuccessful. Here is an attempt that seems intuitively correct to me. Errors: ["Property 'property1' has not been defined and the schema does not allow additional properties.", "Property 'property2' has not been defined and the schema does not allow additional properties."

{
"$schema": "http://json-schema.org/draft/2020-12/schema",
"title": "Title",
"description": "Version...",
"type": "object",
"patternProperties": {
    "^[A-Z0-9]*$": {
        "type": "object",
        "title": "sub-title",
        "description": "...",
        "examples": [
            "OPTION"
        ],
        "oneOf": [
            {
                "type": "object",
                "properties": {
                    "property1": {
                        "type": "string"
                    },
                    "property2": {
                        "type": "integer"
                    }
                }
            },
            {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "property1": {
                            "type": "string"
                        },
                        "property2": {
                            "type": "integer"
                        },
                        "property3": {
                            "type": "boolean"
                        },
                        "additionalProperties": false
                    }
                }
            }
        ],
        "additionalProperties": false
    }
    }
}

Edit: Fixed a bad copy/paste


Solution

  • This is when you want to use unevaluatedProperties instead of additionalProperties.

    additionalProperties can only see the properties that are defined in the immediate schema object. It can't see properties that are defined by subschemas of the immediate schema object.

    {
      "properties": {
        "foo": true              // seen
      },
      "patternProperties": {
        "^[a-z]+[0-9]+$": true   // seen (property requires letters AND numbers)
      },
      "allOf": [
        {
          "properties": {
            "bar": false         // not seen
          }
        }
      ],
      "additionalProperties": false
    }
    

    However, unevaluatedProperties can see inside those subschemas, so for the above, bar could be seen if you used unevaluatedProperties instead.


    You may also want to review your subschema for the new design. The subschema currently describes an array for the value of OPTIONS, but your example for the new data is an object with "optionN" properties.


    Lastly, a little refactoring can help make this a little more readable by isolating each possible form. By using $refs and $defs you can create labeled subschemas that support each form.

    {
      "$schema": "http://json-schema.org/draft/2020-12/schema",
      "title": "Title",
      "description": "Version...",
      "type": "object",
      "patternProperties": {
        "^[A-Z0-9]*$": {
          "type": "object",
          "title": "sub-title",
          "description": "...",
          "examples": [
            "OPTION"
          ],
          "oneOf": [
            { "$ref": "#/$defs/legacy" },
            { "$ref": "#/$defs/theNewWay" }
          ],
          "unevaluatedProperties": false
        }
      },
      "$defs": {
        "legacy": {
          "type": "object",
          "properties": {
            "property1": {
              "type": "string"
            },
            "property2": {
              "type": "integer"
            }
          }
        },
        "theNewWay": {                // obviously rename this :)
          "type": "array",            // (and review this schema, maybe)
          "items": {
            "type": "object",
            "properties": {
              "property1": {
                "type": "string"
              },
              "property2": {
                "type": "integer"
              },
              "property3": {
                "type": "boolean"
              },
              "additionalProperties": false
            }
          }
        }
      }
    }