Is there a way to implement the Postgres equivalent to CHECK constraint within a nested JSON Schema? Say we have data that has two properties, each of which has nested properties. How can JSON Schema make the required contents of the first object depend on the second?
My real case scenario is to build a JSON schema for a GeoJSON objects, that has a geometry object (i.e. Point or Polygon, or null), and other attributes in a "properties" object. I want to alter the required properties depending on the type of geometry.
I failed with both the following solutions:
- Nest "allOf" inside "anyOf" to cover all the possibilities
- Duplicate the "definitions" to have a attributes_no_geom, geometry_no_geom, attribute_with_geom and geometry_with_geom and declare them in a "anyOf"
This would validate since attribute/place covers for the lack of geometry:
{
"attributes": {
"name": "Person2",
"place": "City2"
},
"geometry": null
}
This would also validate since attribute/place is no longer required with a geometry:
{
"attributes": {
"name": "Person1"
},
"geometry": {
"type": "Point",
"coordinates": []
}
}
EDIT
Building on Relequestual's answer, this is the unsatisfactory result I'm getting :
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"geometryIsPoint": {
"type": "object",
"required": ["type"],
"properties": {
"type": {
"const": "Point"
}
}
},
"partialAttributes": {
"type": "object",
"required": ["name"],
"properties": {
"name": {
"type": "string"
},
"place": {
"type": "string"
}
}
},
"fullAttributes": {
"type": "object",
"required": ["name", "place"],
"properties": {
"name": {
"type": "string"
},
"place": {
"type": "string"
}
}
},
"conditionalAttributes": {
"allOf": [
{
"if": {
"$ref": "#/definitions/geometryIsPoint"
},
"then": {
"$ref": "#/definitions/partialAttributes"
},
"else": {
"$ref": "#/definitions/fullAttributes"
}
}
]
}
},
"properties": {
"attributes": {
"$ref": "#/definitions/conditionalAttributes"
},
"geometry": {
"$ref": "#/definitions/geometryIsPoint"
}
}
}
This schema will not validate the following if the attributes/place
property is removed.
{
"attributes": {
"name": "Person",
"place": "INVALID IF THIS LINE IS REMOVED ;-("
},
"geometry": {
"type": "Point",
"coordinates": {}
}
}
You can use if/then/else
keywords to apply subschemas conditionally.
We only want if
and then
for your solution.
The value of both must be a JSON Schema.
If the value of if
results in a positive assertion (when the schema is applied to the instance, and it validates successfully), then the schema value of then
is applied to the instance.
Here's the schema.
I've pre-loaded the schema and data at https://jsonschema.dev so you can test it live.
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"geometryIsPoint": {
"required": [
"type"
],
"properties": {
"type": {
"const": "Point"
}
}
},
"geometryAsPoint": {
"required": [
"coordinates"
],
"properties": {
"coordinates": {
"type": "array"
}
}
},
"geometry": {
"allOf": [
{
"if": {
"$ref": "#/definitions/geometryIsPoint"
},
"then": {
"$ref": "#/definitions/geometryAsPoint"
}
}
]
}
},
"properties": {
"geometry": {
"$ref": "#/definitions/geometry"
}
}
}
The property geometry
references the definition geometry
.
allOf
is an array of schemas.
The value of allOf[0].if
references the schema defined as geometryIsPoint
.
The schema defined as geometryIsPoint
is applied to the geometry
value. If it validates successfully, then the then
referenced schema is applied.
You don't have to use referencing to do any of this, but I feel it makes the intent clearer.
Extend the schema as required, adding schemas to allOf
for as many geometry types as you want to recognise.
Edit:
You were hitting the else
condition of your conditional, because the if
failed validation. Let me explain.
Here's an updated schema to cover your modified use case.
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"geometry": {
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"enum": [
"Point",
"somethingelse",
null
]
}
}
},
"geometryIsPoint": {
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"const": "Point"
}
}
},
"attributes": {
"properties": {
"name": {
"type": "string"
},
"place": {
"type": "string"
}
}
},
"partialAttributes": {
"type": "object",
"required": [
"name"
]
},
"fullAttributes": {
"type": "object",
"required": [
"name",
"place"
]
},
"conditionalAttributes": {
"allOf": [
{
"if": {
"required": [
"geometry"
],
"properties": {
"geometry": {
"$ref": "#/definitions/geometryIsPoint"
}
}
},
"then": {
"required": [
"attributes"
],
"properties": {
"attributes": {
"$ref": "#/definitions/partialAttributes"
}
}
},
"else": {
"required": [
"attributes"
],
"properties": {
"attributes": {
"$ref": "#/definitions/fullAttributes"
}
}
}
}
]
}
},
"properties": {
"attributes": {
"$ref": "#/definitions/attributes"
},
"geometry": {
"$ref": "#/definitions/geometry"
}
},
"allOf": [
{
"$ref": "#/definitions/conditionalAttributes"
}
]
}
Here's a JSON Schema dev link so you can test it.
What we're doing here is splitting up the concerns.
The "shape" of attributes
and geometry
is defined in definitions with the corresponding key. Those schemas do not assert which keys are required in those objects, only what they must be if provided.
Because $ref
in a schema makes all other keywords in a schema ignored (for draft-7 or below), at the root level, I've wrapped the reference to conditionalAttributes
in an allOf
.
conditionalAttributes
is a defined JSON Schema. I've used allOf
so you can add more conditional checks.
The value of conditionalAttributes.allOf[0].if
is a JSON Schema, and is applied to the root of your JSON instance. It requires a key of geometry
and that the value is geometryIsPoint
. (If you omit the required
, you'll end up with validation issues, because omitting that key will then pass the if condition).
When the instance results in a true
assertion (validation valid) for the if
value schema, then the then
value schema is applied at the root level.
Because it's applied at the root level and you want to check the value of a nested property, you have to use properties
as you would if you were at the root level of your schema. THIS is how you do conditional schema application (if/then/else
) across different depths of your instance.
You can test out the conditional resolution by changing one of the schema values to false
and looking at the errors. Remember, true
and false
are valid JSON Schemas, so you can write "then": false
to cause an error if you expect the then
schema to be applied (as in, the if
schema asserted validation OK).