JSON Schema DSL¶
The models generated by by this package form a Domain-specific language (DSL) for writing JSON Schema in Python. This DSL can be used to write JSON Schema directly, or can be generated from JSON Schema documents on the fly.
Primitives¶
Each JSON Schema is described by an Element object. The simplest possible schema (equivalent to {} or true) is expressed as
>>> from statham.dsl.elements import Element
>>> element = Element()
This element will accept any value:
>>> element(1)
1
>>> element("a string!")
"a string"
Validation can be added by using typed elements and keyword arguments:
>>> Element(minimum=3)
>>> String(maxLength=20)
>>> Boolean(default=True)
The following primitive elements are available:
Containers¶
Elements accepting list and dict values include schemas for validating their contained items. When called, these elements will recursively validate both the container and its contained items.
Array¶
Array accepts an Element as its only positional argument. This corresponds to the "items" JSON Schema keyword.
>>> from statham.dsl.elements import Array, String
>>> array = Array(String())
>>> array(["a", "string"])
["a", "string"]
>>> array([1, 2])
ValidationError: Failed validating `1`. Must be of type (str).
Array will also accept a list of elements as its "items". In this case, each list item will be validated against the Element at the corresponding index:
>>> from statham.dsl.elements import Array, Integer, String
>>> array = Array([Integer(), String()])
>>> array([1, "a string"])
[1, "a string"]
>>> array(["two", "strings"])
ValidationError: Failed validating `'two'`. Must be of type (int).
When items schemas are declared in this way, subsequent elements are validated by the additionalItems option, which by default allows anything.
>>> array([1, "a string", 23.0]) # Accepts any additional items
[1, "a string", 23.0]
>>> array = Array([Integer(), String()], additionalItems=False)
>>> array([1, "a string", 23.0])
ValidationError: Failed validating `[1, 'string', 23.0]`. Must not contain additional items. Accepts: [Integer(), String()]
>>> array = Array([Integer(), String()], additionalItems=Number())
>>> array([1, "a string", 23.0])
[1, "a string", 23.0]
>>> array([1, "a string", "an unexpected string"])
ValidationError: Failed validating `'an unexpected string'`. Must be of type (float,int).
Object¶
Object is a special case, and key to leveraging type-checking with the DSL. Object-typed schemas are declared as sub-classes of Object.
>>> from statham.dsl.constants import Maybe
>>> from statham.dsl.elements import Object, String
>>> from statham.dsl.property import Property
>>>
>>> class StringWrapper(Object):
... value: Maybe[str] = Property(String())
>>>
>>> StringWrapper({"value": "a string"})
StringWrapper(value='a string')
The Property descriptor is used to declare which properties are required, and to rename properties which aren’t valid python attributes:
>>> class CustomObject(Object):
... class_: str = Property(String(), required=True, source="class")
>>>
>>> CustomObject({"class": "ABC"})
CustomObject(class_='ABC')
By default, properties are not required, and do not need to be present when instantiating the class. The statham.dsl.constants.Maybe generic type is used to annotate this (see first example).
Additional keywords may be set on the schema via class arguments:
>>> class StringWrapper(Object, additionalProperties=False):
... value: str = Property(String())
>>>
>>> StringWrapper({"other": "a string"})
ValidationError: Failed validating `{'other': 'a string'}`. Must not contain unspecified properties. Accepts: {'value'}
Properties which are accepted via additionalProperties or patternProperties are accessible via __getitem__():
>>> class StringWrapper(Object):
... value = Property(String())
>>>
>>> value = StringWrapper({"value": "a string", "other": "another string"})
>>> value["other"]
"another string"
Composition¶
Elements for composition keywords (e.g. "not", "anyOf", "oneOf", "allOf") break from the standard JSON Schema structure. The DSL does not allow outer keywords when a composition keyword is present, with the exception of the "default" keyword. This reduces the number of possible ways to write the same schema, without making any schema impossible.
For example, consider the following schema which allows any string, provided it is not a UUID.
{
"type": "string",
"not": {"format": "uuid"}
}
The equivalent form is achieved in the DSL with AllOf:
from statham.dsl.elements import (
AllOf,
Element,
Not,
String,
)
element = AllOf(String(), Not(Element(format="uuid")))
Similarly, schemas with multiple types are achieved with AnyOf:
{
"type": ["string", "integer"]
}
may be expressed as
from statham.dsl.elements import AnyOf, Integer, String
element = AnyOf(String(), Integer())
There are four composition elements available:
Parsing JSON Schema Documents¶
JSON Schema documents can be directly parsed to DSL elements, without generating any code. This reduces the benefit gained by type hints, but can still be useful for inspecting JSON Schemas in Python, and using functionality like "default".
For simple schemas, with no definitions, parse_element() can be used.
>>> from statham.dsl.parser import parse_element
>>> parse_element({"type": "string", "maxLength": 20})
String(maxLength=20)
If your schema contains multiple definitions, and you’d like to parse all of them, then use parse(). This will return a list of elements, starting with the top-level schema, followed by schemas found in definitions. Be aware that leaving the top-level empty will be parsed (correctly) as a blank schema, or Element().
Note
These parsing tools make the following assumptions:
The schema has already been dereferenced
Any
"object"schemas have a"title"annotation
statham uses another library to do this automatically when performing code generation, you can do it yourself like so:
>>> from json_ref_dict import materialize, RefDict
>>> from statham.titles import title_labeller
>>>
>>> schema = materialize(
... RefDict.from_uri(<uri>), context_labeller=title_labeller()
>>> )
For more information about what this is doing, look at json-ref-dict.