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
andthen
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 ofthen
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.
The property
geometry
references the definitiongeometry
.allOf
is an array of schemas.The value of
allOf[0].if
references the schema defined asgeometryIsPoint
.The schema defined as
geometryIsPoint
is applied to thegeometry
value. If it validates successfully, then thethen
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 theif
failed validation. Let me explain.Here's an updated schema to cover your modified use case.
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
andgeometry
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 toconditionalAttributes
in anallOf
.conditionalAttributes
is a defined JSON Schema. I've usedallOf
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 ofgeometry
and that the value isgeometryIsPoint
. (If you omit therequired
, 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 theif
value schema, then thethen
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
andfalse
are valid JSON Schemas, so you can write"then": false
to cause an error if you expect thethen
schema to be applied (as in, theif
schema asserted validation OK).