2
votes

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": {}
    }
}
1
You can use the if/then/else keywords, wrapped inside an allOf to achive what you want here. I hope to provide you with a fuller answer sometime today.Relequestual
I'm assuming you are using a library that supports draft-7 JSON Schema. Is that correct?Relequestual
Yes, I'm using draft-7 JSON Schema. Thank you for the suggestion. I will give conditionals a try but my understanding of the docs was that the content of the conditional blocks always refered to the properties they were declared in, therefore, in the example above, I could not do something like if geometry/type then require something in attributes?Lecram
It IS possible. I'll show you how soon =]Relequestual

1 Answers

0
votes

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).