JSON schema : "allof" with "additionalProperties"
Suppose we have schema following schema (from tutorial here):
{
"$schema": "http://json-schema.org/draft-04/schema#",
"definitions": {
"address": {
"type": "object",
"properties": {
"street_address": { "type": "string" },
"city": { "type": "string" },
"state": { "type": "string" }
},
"required": ["street_address", "city", "state"]
}
},
"type": "object",
"properties": {
"billing_address": { "$ref": "#/definitions/address" },
"shipping_address": {
"allOf": [
{ "$ref": "#/definitions/address" },
{ "properties":
{ "type": { "enum": [ "residential", "business" ] } },
"required": ["type"]
}
]
}
}
}
And here is valid instance:
{
"shipping_address": {
"street_address": "1600 Pennsylvania Avenue NW",
"city": "Washington",
"state": "DC",
"type": "business"
}
}
I need to ensure that any additional fields for shipping_address
will be invalid. I know for this purpose exists additionalProperties
which should be set to "false". But when I'm setting "additionalProprties":false
as in the following:
"shipping_address": {
"allOf": [
{ "$ref": "#/definitions/address" },
{ "properties":
{ "type": { "enum": [ "residential", "business" ] } },
"required": ["type"]
}
],
"additionalProperties":false
}
I get a validation error (checked here):
[ {
"level" : "error",
"schema" : {
"loadingURI" : "#",
"pointer" : "/properties/shipping_address"
},
"instance" : {
"pointer" : "/shipping_address"
},
"domain" : "validation",
"keyword" : "additionalProperties",
"message" : "additional properties are not allowed",
"unwanted" : [ "city", "state", "street_address", "type" ]
} ]
The question is: how should I to limit fields for the shipping_address
part only? Thanks in advance.
Solution 1:
[author of the draft v4 validation spec here]
You have stumbled upon the most common problem in JSON Schema, that is, its fundamental inability to do inheritance as users expect; but at the same time it is one of its core features.
When you do:
"allOf": [ { "schema1": "here" }, { "schema2": "here" } ]
schema1
and schema2
have no knowledge of one another; they are evaluated in their own context.
In your scenario, which many, many people encounter, you expect that properties defined in schema1
will be known to schema2
; but this is not the case and will never be.
This problem is why I have made these two proposals for draft v5:
-
strictProperties
, -
merge
.
Your schema for shipping_address
would then be:
{
"merge": {
"source": { "$ref": "#/definitions/address" },
"with": {
"properties": {
"type": { "enum": [ "residential", "business" ] }
}
}
}
}
along with defining strictProperties
to true
in address
.
Incidentally, I am also the author of the website you are referring to.
Now, let me backtrack to draft v3. Draft v3 did define extends
, and its value was either of a schema or an array of schemas. By the definition of this keyword, it meant that the instance had to be valid against the current schema and all schemas specified in extends
; basically, draft v4's allOf
is draft v3's extends
.
Consider this (draft v3):
{
"extends": { "type": "null" },
"type": "string"
}
And now, that:
{
"allOf": [ { "type": "string" }, { "type": "null" } ]
}
They are the same. Or maybe that?
{
"anyOf": [ { "type": "string" }, { "type": "null" } ]
}
Or that?
{
"oneOf": [ { "type": "string" }, { "type": "null" } ]
}
All in all, this means that extends
in draft v3 never really did what people expected it to do. With draft v4, *Of
keywords are clearly defined.
But the problem you have is the most commonly encountered problem, by far. Hence my proposals which would quench this source of misunderstanding once and for all!
Solution 2:
additionalProperties
applies to all properties that are not accounted-for by properties
or patternProperties
in the immediate schema.
This means that when you have:
{
"allOf": [
{ "$ref": "#/definitions/address" },
{ "properties":
{ "type": { "enum": [ "residential", "business" ] } },
"required": ["type"]
}
],
"additionalProperties":false
}
additionalProperties
here applies to all properties, because there is no sibling-level properties
entry - the one inside allOf
does not count.
One thing you could do is to move the properties
definition one level up, and provide stub entries for properties you are importing:
{
"allOf": [{"$ref": "#/definitions/address"}],
"properties": {
"type": {"enum": ["residential", "business"]},
"addressProp1": {},
"addressProp2": {},
...
},
"required": ["type"],
"additionalProperties":false
}
This means that additionalProperties
will not apply to the properties you want.
Solution 3:
Here's a slightly simplified version of Yves-M's Solution:
{
"$schema": "http://json-schema.org/draft-04/schema#",
"definitions": {
"address": {
"type": "object",
"properties": {
"street_address": {
"type": "string"
},
"city": {
"type": "string"
},
"state": {
"type": "string"
}
},
"required": [
"street_address",
"city",
"state"
]
}
},
"type": "object",
"properties": {
"billing_address": {
"$ref": "#/definitions/address"
},
"shipping_address": {
"allOf": [
{
"$ref": "#/definitions/address"
}
],
"properties": {
"type": {
"enum": [
"residential",
"business"
]
},
"street_address": {},
"city": {},
"state": {}
},
"required": [
"type"
],
"additionalProperties": false
}
}
}
This preserves the validation of required properties in the base address
schema, and just adds the required type
property in the shipping_address
.
It's unfortunate that additionalProperties
only takes the immediate, sibling-level properties into account. Maybe there is a reason for this. But this is why we need to repeat the inherited properties.
Here, we're repeating the inherited properties in simplified form, using empty object syntax. This means that properties with these names would be valid no matter what kind of value they contained. But we can rely on the allOf
keyword to enforce the type constraints (and any other constraints) declared in the base address
schema.
Solution 4:
Don't set additionalProperties=false at definition level
And everything will be fine:
{
"definitions": {
"address": {
"type": "object",
"properties": {
"street_address": { "type": "string" },
"city": { "type": "string" },
"state": { "type": "string" }
}
}
},
"type": "object",
"properties": {
"billing_address": {
"allOf": [
{ "$ref": "#/definitions/address" }
],
"properties": {
"street_address": {},
"city": {},
"state": {}
},
"additionalProperties": false
"required": ["street_address", "city", "state"]
},
"shipping_address": {
"allOf": [
{ "$ref": "#/definitions/address" },
{
"properties": {
"type": {
"enum": ["residential","business"]
}
}
}
],
"properties": {
"street_address": {},
"city": {},
"state": {},
"type": {}
},
"additionalProperties": false
"required": ["street_address","city","state","type"]
}
}
}
Each of your billing_address
and shipping_address
should specify their own required properties.
Your definition should not have "additionalProperties": false
if you want to combine his properties with other ones.