Design pattern: Using "propertyNames" to catch unexpected properties. #77
Description
Note: Originally filed as json-schema-org/json-schema-spec#214, but really more of a usage to document and explain rather than an issue for the spec.
Now that we have "propertyNames"
, it is easier to cause validation to fail on unexpected properties without using "additionalProperties": false
. The general idea is that names in "properties"
become an "enum"
in "propertyNames"
, while each pattern in "patternProperties"
becomes a "pattern"
in "propertyNames"
.
It seems like this would be worth writing up as a possible solution on the web site. The following description is not the most concise and confused at least one person on the old issue, so it's worth discussing some here to find a good way to present this.
While the names and patterns involved still must be listed twice, it is possible to do this for each schema that needs to be re-used. Then, unlike with "additionalProperties"
, you can combine them however you want. This makes the process relatively easy to manage with well-structured "definitions"
(and perhaps we can come up with a way to make it simpler?)
{
"definitions": {
"fooSchema": {
"properties": {
"foo1": {"type": "number"},
"foo2": {"type": "boolean"}
},
"patternProperties": {
"foo[A-Z][a-z0-9]*": {"type": "string"}
}
},
"fooProperties": {
"propertyNames": {
"$comment": "Need to anyOf these or else the enum and pattern conflict",
"anyOf": [
{"enum": ["foo1", "foo2"]},
{"pattern": "foo[A-Z][a-z0-9]*"}
]
}
},
"barSchema": {
"properties": {
"bar1": {"type": "null"},
"bar2": {"type": "integer"}
}
},
"barProperties": {
"propertyNames": {"enum": ["bar1", "bar2"]}
}
},
"$comment": "Even with allOf schemas, need anyOf propertyNames or else they conflict",
"allOf": [
{"$ref": "#/definitions/fooSchema"},
{"$ref": "#/definitions/barSchema"}
],
"anyOf": [
{"$ref": "#/definitions/fooProperties"},
{"$ref": "#/definitions/barProperties"}
]
}
This is much better than:
{
"definitions": {
"fooSchema": {
"properties": {
"foo1": {"type": "number"},
"foo2": {"type": "boolean"}
},
"patternProperties": {
"foo[A-Z][a-z0-9]*": {"type": "string"}
}
},
"barSchema": {
"properties": {
"bar1": {"type": "null"},
"bar2": {"type": "integer"}
}
}
},
"allOf": [
{"$ref": "#/definitions/fooSchema"},
{"$ref": "#/definitions/barSchema"},
{
"properties": {
"foo1": true,
"foo2": true,
"bar1": true,
"bar2": true
},
"patternProperties": {
"foo[A-Z][a-z0-9]*": true
},
"additionalProperties": false
}
]
}
In this version, you have to construct the correct "additionalProperties": false
schema for every combination individually. With the "anyOf"
+ "propertyNames"
approach, you can package up the property name rules and re-use them as easily as you do the corresponding schemas.
While this is not the simplest construct possible, it is much simpler than "$combine"/"$combinable"
, (#119) and unlike "$merge"
/"$patch"
(#15) it cannot be abused to produce JSON that is not even a schema, or to splice things in ways that authors of the source of the splice never intended.
I feel like this is a more promising avenue than the other proposals so far. Would it be reasonable to recommend this and put some guidance on the web site and see how that goes in draft 6? Then maybe we'll find that we don't really need the more complex solutions at all.