-
Notifications
You must be signed in to change notification settings - Fork 2
Description
TLDR; summary: use "unevaluatedProperties": false when inheritance is involved (if using JSON Schema Draft >= 2019-09) to be able to reject unexpected members
Long version:
Let's play a bit with the example of "Best Practice for OGC - UML to JSON encoding rules" (https://portal.ogc.org/files/?artifact_id=108010&version=1#toc27), paragraph 7.3.3.4 "Inheritance"
Let's consider:
- an instance document
test.jsoncontaining:
{
"propertyA": 2,
"propertyB": "x"
}- the (actually "one among many possibilities") corresponding JSON schema,
test.schema.jsondefining a class TypeA with a propertyA and a class TypeB, extending TypeA, with a propertyB:
{
"$schema": "http://json-schema.org/draft/2020-12/schema",
"$defs": {
"TypeA": {
"properties": {
"propertyA": {
"type": "number"
}
},
"required": [
"propertyA"
]
},
"TypeB": {
"allOf": [
{
"$ref": "#/$defs/TypeA"
},
{
"type": "object",
"properties": {
"propertyB": {
"type": "string"
}
},
"required": [
"propertyB"
]
}
]
}
},
"$ref": "#/$defs/TypeB"
}This is a direct copy&paste from the above reference best practice document
Now let's use the check-jsonschema Python utility (using latest version 0.29.2 at time of writing):
$ check-jsonschema --schemafile test.schema.json test.json
ok -- validation done
Life is good as expected.
Now let's consider a variation of our document with a unexpected property. Let's call it test_unexpected.json :
{
"propertyA": 2,
"propertyB": "x",
"unexpected": "foo"
}Let's validate it:
$ check-jsonschema --schemafile test.schema.json test_unexpected.json
ok -- validation done
The success of test_unexpected is expected (pun intended). The reason is that by default a JSON schema allows additional properties. You need to use an explicit keyword in a JSON schema to disallow them: additionalProperties: false. That's one of the key thing I wanted for the PROJJSON schema. When there are non-mandatory members, I wanted to be able to catch typos, by rejecting additional properties. That way it would catch someone mispelling "datum_ensemble" as "datum_ensembble" , or putting a lowercase where an uppercase is expected, or forgetting a underscore, or whatever.
And that's where things are tricky. My findings is that inheritance using allOff is fine if used alone, additionalProperties: false is fine if used alone, but if you start combining them together, things break apart.
Let's see by modifying test.schema.json as test_add_properties_false.schema.json with
{
"$schema": "http://json-schema.org/draft/2020-12/schema",
"$defs": {
"TypeA": {
"properties": {
"propertyA": {
"type": "number"
}
},
"required": [
"propertyA"
]
},
"TypeB": {
"allOf": [
{
"$ref": "#/$defs/TypeA"
},
{
"type": "object",
"properties": {
"propertyB": {
"type": "string"
}
},
"required": [
"propertyB"
]
}
],
"additionalProperties": false
}
},
"$ref": "#/$defs/TypeB"
}Now let's validate the doc that has the unexpected member:
$ check-jsonschema --schemafile test_add_properties_false.schema.json test_unexpected.json
Schema validation errors were encountered.
test_unexpected.json::$: Additional properties are not allowed ('propertyA', 'propertyB', 'unexpected' were unexpected)
It gets rejected, but or a wrong reason. It also considers propertyA and propertyB to be unexpected.
OK, let's try with a valid instance:
$ check-jsonschema --schemafile test_add_properties_false.schema.json test.json
Schema validation errors were encountered.
test.json::$: Additional properties are not allowed ('propertyA', 'propertyB' were unexpected)
Same issue. That's not good. So this is not the way to go.
Let's experiment with another approach for test.json.schema. Let's call it test2.json.schema:
{
"$schema": "http://json-schema.org/draft/2020-12/schema",
"$defs": {
"TypeA": {
"properties": {
"propertyA": {
"type": "number"
}
},
"required": [
"propertyA"
]
},
"TypeB": {
"type": "object",
"allOf": [{"$ref": "#/$defs/TypeA"}],
"properties": {
"propertyB": {
"type": "string"
}
},
"required": [
"propertyB"
]
}
},
"$ref": "#/$defs/TypeB"
}
So basically we move the allOf constraint inside typeB
Valid documents still validate:
$ check-jsonschema --schemafile test2.schema.json test.json
ok -- validation done
$ check-jsonschema --schemafile test2.schema.json test_unexpected.json
ok -- validation done
Now let's add additionalProperties:false in it to create a test2_add_properties_false.schema.json schema:
{
"$schema": "http://json-schema.org/draft/2020-12/schema",
"$defs": {
"TypeA": {
"properties": {
"propertyA": {
"type": "number"
}
},
"required": [
"propertyA"
]
},
"TypeB": {
"type": "object",
"allOf": [{"$ref": "#/$defs/TypeA"}],
"properties": {
"propertyB": {
"type": "string"
}
},
"required": [
"propertyB"
],
"additionalProperties": false
}
},
"$ref": "#/$defs/TypeB"
}
Let' s try it:
$ check-jsonschema --schemafile test2_add_properties_false.schema.json test.json
Schema validation errors were encountered.
test.json::$: Additional properties are not allowed ('propertyA' was unexpected)
$ check-jsonschema --schemafile test2_add_properties_false.schema.json test_unexpected.json
Schema validation errors were encountered.
test_unexpected.json::$: Additional properties are not allowed ('propertyA', 'unexpected' were unexpected)
So things are slightly better in which it doesn't reject anymore propertyB, but it rejects the inherited propertyA.
This is why in projjsonschema, I found out that you have to re-mention inherits member of upper classes. So let's try that and create test_redefine_everything.schema.json with:
{
"$schema": "http://json-schema.org/draft/2020-12/schema",
"$defs": {
"TypeA": {
"properties": {
"propertyA": {
"type": "number"
}
},
"required": [
"propertyA"
]
},
"TypeB": {
"type": "object",
"allOf": [{"$ref": "#/$defs/TypeA"}],
"properties": {
"propertyA": {},
"propertyB": {
"type": "string"
}
},
"required": [
"propertyA",
"propertyB"
],
"additionalProperties": false
}
},
"$ref": "#/$defs/TypeB"
}And now we get the expected results!
$ check-jsonschema --schemafile test_redefine_everything.schema.json test.json
ok -- validation done
$ check-jsonschema --schemafile test_redefine_everything.schema.json test_unexpected.json
Schema validation errors were encountered.
test_unexpected.json::$: Additional properties are not allowed ('unexpected' was unexpected)
Which correlates with the practice in projjson.schema.json, like the following one where geodetic_crs has to mention the members inherited from object_usage:
"geodetic_crs": {
"type": "object",
"properties": {
"type": { "type": "string", "enum": ["GeodeticCRS", "GeographicCRS"] },
"name": { "type": "string" },
"datum": {
"oneOf": [
{ "$ref": "#/definitions/geodetic_reference_frame" },
{ "$ref": "#/definitions/dynamic_geodetic_reference_frame" }
]
},
"datum_ensemble": { "$ref": "#/definitions/datum_ensemble" },
"coordinate_system": { "$ref": "#/definitions/coordinate_system" },
"deformation_models": {
"type": "array",
"items": { "$ref": "#/definitions/deformation_model" }
},
"$schema" : {},
"scope": {},
"area": {},
"bbox": {},
"vertical_extent": {},
"temporal_extent": {},
"usages": {},
"remarks": {},
"id": {}, "ids": {}
},
"required" : [ "name" ],
"description": "One and only one of datum and datum_ensemble must be provided",
"allOf": [
{ "$ref": "#/definitions/object_usage" },
{ "$ref": "#/definitions/one_and_only_one_of_datum_or_datum_ensemble" }
],
"additionalProperties": false
},This is why I say that inheritance in JSON schema is not a prime concept.
But... all of that was true in the draf-07 era. draft 2019-09 brings a new keyword, "unevaluatedProperties", which is a kind of "additionalProperties", but that works with inheritance: https://json-schema.org/understanding-json-schema/reference/object#unevaluatedproperties
So let's now slightly edit the original test.schema.json as test_unevaluatedProperties.schema.json by adding a unevaluatedProperties: false member in it:
{
"$schema": "http://json-schema.org/draft/2020-12/schema",
"$defs": {
"TypeA": {
"properties": {
"propertyA": {
"type": "number"
}
},
"required": [
"propertyA"
]
},
"TypeB": {
"allOf": [
{
"$ref": "#/$defs/TypeA"
},
{
"type": "object",
"properties": {
"propertyB": {
"type": "string"
}
},
"required": [
"propertyB"
]
}
],
"unevaluatedProperties": false
}
},
"$ref": "#/$defs/TypeB"
}And now:
$ check-jsonschema --schemafile test_unevaluatedProperties.schema.json test_une
test_unevaluatedProperties.schema.json test_unexpected.json
$ check-jsonschema --schemafile test_unevaluatedProperties.schema.json test_unexpected.json
Schema validation errors were encountered.
test_unexpected.json::$: Unevaluated properties are not allowed ('unexpected' was unexpected)
Yes!!! Success. So the Best Practice document should be updated to mention that "unevaluatedProperties": false must be used with inheritance if using JSON Schema Draft >= 2019-09. And this is the key to simplify projjson.schema.json without all the tedious copy&paste bloat I had to do at the time of Draft 07 (PROJJSON was initially released in July 2019)