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
(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 :-)
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, blueberryNotice 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
-- blueberryThere'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.