Search code examples
jsonschema

exact definition of unevaluatedProperties in jsonschema


There is the unevaluatedProperties keyword in newer jsonschema drafts and I understand what the purpose of this is.

It was introduced because the "additionalProperties: false" idiom that disallows unknown properties makes schemas non-extendable: We cannot extend the schema without modifying it, putting it in an allOf construct will not work. For that there is now unevaluatedProperties.

The spec can be found here https://json-schema.org/draft/2020-12/json-schema-core#section-11.3.

But I want to understand the exact definition of unevaluatedProperties on a technical level. I cannot really make sense of the definition

Validation with "unevaluatedProperties" applies only to the child values of instance names that do not appear in the "properties", "patternProperties", "additionalProperties", or "unevaluatedProperties" annotation results that apply to the instance location being validated.

What does this mean?

Especially:

  • ... applies only to the child values of instance names ... How can instance names have child values?

  • instance names that .. do not appear in the [...] "additionalProperties" .... How can that be? additionalProperties does not list any names (contrary to properties or patternProperties which at least list a name pattern).

Maybe some example: What is the intended behavior of the following jsonschema?

{ 
  "anyOf": [
     true,
     "not": { "properties": { "a": true } }   // <<<< other lines below
  ], 
  "unevaluatedProperties": false  
}

Interesting JSON candidate instances are {}, { "a": 1 } { "b": 1 }

Variants of the above schema are with the "<<<<" line replaced with

  • "not": { "properties": { "a": false }
  • "not": { "additionalProperties": false }
  • "not": { "additionalProperties": true }

(I strongly suspect that the "not" construct is not really changing anything, but lets include it for fun. Or change it to a solitary if keyword.)

I am not asking what validators are responding, but what the correct behavior according to the schema spec is. With explanation please :-)


Solution

  • I think you're right that the passage you quoted doesn't make sense and needs to be rewritten. So, instead of trying to make it make sense, I'm going to ignore what it says and just describe how unevaluatedProperties is intended to work.

    unevaluatedProperties works by using annotations collected by evaluating the properties, patternProperties, and additionalProperties keywords. Each of these keywords collects the property names that they successfully evaluate.

    {
      "properties": {
        "foo": { "type": "number" },
      },
      "patternProperties": {
        "^b": { "type": "boolean" }
      },
      "unevaluatedProperties": false
    }
    

    Given the instance { "foo": 42, "bar": false, "baz": null }, properties contributes "foo" and patternProperties contributes "bar". That makes the set of evaluated properties, "foo", "bar". unevaluatedProperties then uses that set to determine that "baz" is unevaluated and validates it against the false schema making the instance fail validation.

    Unlike additionalProperties, the set of evaluated properties that's constructed is keyed off of the instance location being evaluated rather than the instance location and the schema location.

    {
      "type": "object",
      "properties": {
        "a": {
          "type": "object",
          "allOf": [
            {
              "properties": {
                "apple": true
              }
            },
            {
              "properties": {
                "apricot": true
              }
            },
          ],
          "unevaluatedProperties": false
        },
        "b": {
          "type": "object",
          "allOf": [
            {
              "properties": {
                "banana": true
              }
            },
            {
              "properties": {
                "blueberry": true
              }
            },
          ],
          "unevaluatedProperties": false
        }
      }
    }
    

    Here, we have three locations that define objects that the validator will collect evaluated properties for.

    • # -- a, b
    • #/a -- apple, apricot
    • #/b -- banana, blueberry

    Notice that the nesting of the schemas doesn't matter because the schema nesting doesn't effect the location being evaluated.

    The set of properties used for additionalProperties also keys off of the schema location, which is why the schema nesting matters for additionalProperties and not unevaluatedProperties.

    • #, # -- a, b
    • #/a, #/properties/a --
    • #/a, #/properties/a/allOf/0 -- apple
    • #/a, #/properties/a/allOf/1 -- apricot
    • #/b, #/properties/b --
    • #/b, #/properties/b/allOf/0 -- banana
    • #/b, #/properties/b/allOf/1 -- blueberry

    There's one more complication to consider. When a schema fails validation, any properties it would otherwise contribute to the set of evaluated properties gets dropped.

    {
      "type": "object",
      "anyOf": [
        {
          "properties": {
            "foo": { "type": "number" }
          }
        },
        {
          "properties": {
            "foo": { "type": "number", "minimum": 0 },
            "bar": { "type": "boolean" }
          }
        }
      ],
      "unevaluatedProperties": false
    }
    

    Consider the instance { "foo": -1, "bar": null }. In /anyOf/1, "bar" is successfully evaluated and "foo" is not. Because the schema as a whole fails, its evaluated properties get dropped. So, "bar" is considered unevaluated and the validation fails.

    An interesting consequence of this is that anything in not never ends up getting collected because the failed validation drops anything that it would have contributed.

    {
      "not": {
        "not": {
          "properties": {
            "foo": { "type": "number" }
          }
        }
      },
      "unevaluatedProperties": false
    }
    

    You would think that double negation is a no-op, but it actually results in dropping all collected properties. So, { "foo": 42 } will pass the not, but will fail unevaluatedProperties and the validation will fail. Only the empty object will pass this schema.