def test_generic_schema(): assert deserialization_schema(DataGeneric, all_refs=True) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "type": "object", "properties": {"a": {}}, "required": ["a"], "additionalProperties": False, } assert deserialization_schema(DataGeneric[int], all_refs=True) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "type": "object", "properties": {"a": {"type": "integer"}}, "required": ["a"], "additionalProperties": False, } assert deserialization_schema(DataGeneric[str], all_refs=True) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "$ref": "#/$defs/StrData", "$defs": { "StrData": { "type": "object", "properties": {"a": {"type": "string"}}, "required": ["a"], "additionalProperties": False, } }, }
def test_flattened_converted_error(): with raises(TypeError): deserialize(Data3, {"attr": 0}) with raises(TypeError): serialize(Data3, Data3(Field2(0))) with raises(TypeError): deserialization_schema(Data3) with raises(TypeError): serialization_schema(Data3) with raises(TypeError): graphql_schema(query=[get_data3])
def test_annotated_schema(): assert (deserialization_schema(A) == serialization_schema(A) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "type": "object", "properties": { "a": { "type": "integer", "maximum": 10, "minimum": 0, "description": "field description", } }, "required": ["a"], "additionalProperties": False, }) assert (deserialization_schema(A, all_refs=True) == serialization_schema( A, all_refs=True) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "$ref": "#/$defs/A", "$defs": { "A": { "additionalProperties": False, "properties": { "a": { "$ref": "#/$defs/someInt", "description": "field description", "minimum": 0, } }, "required": ["a"], "type": "object", }, "someInt": { "description": "type description", "maximum": 10, "type": "integer", }, }, }) assert (print_schema(graphql_schema(query=[a])) == '''\ type Query { a: A! } type A { """field description""" a: someInt! } """type description""" scalar someInt ''')
def test_global_aliaser(): settings.aliaser(camel_case=True) assert deserialization_schema(CamelCase)["properties"] == { "snakeCase": { "type": "integer" } } settings.aliaser(camel_case=False) # dataclasses cache is reset assert deserialization_schema(CamelCase)["properties"] == { "snake_case": { "type": "integer" } }
def test_typed_dict(): assert (deserialization_schema(TD3) == serialization_schema(TD3) == { "type": "object", "properties": { "key1": { "type": "string" }, "key2": { "type": "integer" }, "key3": { "type": "boolean" }, }, "required": ["key2"], "additionalProperties": False, "$schema": "http://json-schema.org/draft/2019-09/schema#", }) assert deserialize(TD3, { "Key2": 0, "Key3": True }, aliaser=str.capitalize) == { "key2": 0, "key3": True, } with raises(ValidationError): assert deserialize(TD3, {}) assert serialize(TD1, {"key1": ""}) == {"key1": ""}
def test_lazy_dataclass_model(): assert deserialize(Lazy, {"a": 0}) == Lazy(0) assert serialize(Lazy(0)) == {"a": 0} assert deserialization_schema(Lazy) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "type": "object", "properties": { "a": { "type": "integer" } }, "required": ["a"], "additionalProperties": False, }
def test_simple_dataclass_model(d_conv, s_conv, alias): assert deserialize(Data, {alias: 0}, conversion=d_conv) == Data(0) assert serialize(Data, Data(0), conversion=s_conv) == {alias: 0} assert (deserialization_schema(Data, conversion=d_conv) == serialization_schema(Data, conversion=s_conv) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "type": "object", "properties": { alias: { "type": "integer" } }, "required": [alias], "additionalProperties": False, })
def test_flattened_schema(): assert deserialization_schema(WithSchema) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "type": "object", "properties": { "attr1": { "type": "integer", "minimum": 3 }, "attr2": { "type": "integer", "minimum": 2 }, }, "required": ["attr1", "attr2"], "additionalProperties": False, }
def test_flattened_converted(): data2 = deserialize(Data2, {"attr": 0}) assert isinstance(data2.data_field2, Field2) and data2.data_field2.attr == 0 assert serialize(Data2, data2) == {"attr": 0} assert (deserialization_schema(Data) == serialization_schema(Data) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "type": "object", "allOf": [ { "type": "object", "additionalProperties": False }, { "type": "object", "properties": { "attr": { "type": "integer" } }, "required": ["attr"], "additionalProperties": False, }, ], "unevaluatedProperties": False, }) schema = graphql_schema(query=[get_data2]) assert graphql_sync(schema, "{getData2{attr}}").data == { "getData2": { "attr": 0 } } assert (print_schema(schema) == """\ type Query { getData2: Data2! } type Data2 { attr: Int! } """)
def test_recursive_by_conversion_schema(): assert deserialization_schema(Foo) == { "$ref": "#/$defs/Foo", "$defs": { "Foo": { "type": "object", "properties": { "foo": { "anyOf": [{ "$ref": "#/$defs/Foo" }, { "type": "null" }] } }, "required": ["foo"], "additionalProperties": False, } }, "$schema": "http://json-schema.org/draft/2019-09/schema#", }
definitions_schema, deserialization_schema, ) @dataclass class Bar: baz: Optional[int] @dataclass class Foo: bar: Bar assert deserialization_schema(Foo, all_refs=True) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "$ref": "#/$defs/Foo", "$defs": { "Foo": { "type": "object", "properties": { "bar": { "$ref": "#/$defs/Bar" } }, "required": ["bar"], "additionalProperties": False, }, "Bar": { "type": "object",
from dataclasses import dataclass from apischema.json_schema import deserialization_schema @dataclass class Foo: bar: int def ref_factory(ref: str) -> str: return f"http://some-domain.org/path/to/{ref}.json#" assert deserialization_schema(Foo, ref_factory=ref_factory) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "$ref": "http://some-domain.org/path/to/Foo.json#", }
from enum import Enum from apischema import deserialize, serialize from apischema.conversions import as_names from apischema.json_schema import deserialization_schema, serialization_schema @as_names class MyEnum(Enum): FOO = object() BAR = object() assert deserialize(MyEnum, "FOO") == MyEnum.FOO assert serialize(MyEnum, MyEnum.FOO) == "FOO" assert ( deserialization_schema(MyEnum) == serialization_schema(MyEnum) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "type": "string", "enum": ["FOO", "BAR"], } )
# Deserialize data resource = deserialize(Resource, data) assert resource == Resource(uuid, "wyfo", {"some_tag"}) # Serialize objects assert serialize(resource) == data # Validate during deserialization with raises(ValidationError) as err: # pytest check exception is raised deserialize(Resource, {"id": "42", "name": "wyfo"}) assert serialize(err.value) == [ # ValidationError is serializable { "loc": ["id"], "err": ["badly formed hexadecimal UUID string"] } ] # Generate JSON Schema assert deserialization_schema(Resource) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "tags": { "type": "array", "items": { "type": "string" },
metadata=alias("$defs")) # This field schema is flattened inside the owning one json_schema: JsonSchema = field(default=JsonSchema(), metadata=flatten) data = { "$schema": "http://json-schema.org/draft/2019-09/schema#", "title": "flattened example", } root_schema = RootJsonSchema( schema="http://json-schema.org/draft/2019-09/schema#", json_schema=JsonSchema(title="flattened example"), ) assert deserialize(RootJsonSchema, data) == root_schema assert serialize(RootJsonSchema, root_schema) == data assert deserialization_schema(RootJsonSchema) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "$defs": { "JsonSchema": { "type": "object", "properties": { "title": { "type": "string" }, "description": { "type": "string" }, "format": { "type": "string" },
from dataclasses import dataclass, field from typing import Any from apischema.json_schema import deserialization_schema, serialization_schema from apischema.metadata import skip @dataclass class Foo: bar: Any deserialization_only: Any = field(metadata=skip(serialization=True)) serialization_only: Any = field(default=None, metadata=skip(deserialization=True)) baz: Any = field(default=None, metadata=skip) assert deserialization_schema(Foo) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "type": "object", "properties": {"bar": {}, "deserialization_only": {}}, "required": ["bar", "deserialization_only"], "additionalProperties": False, } assert serialization_schema(Foo) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "type": "object", "properties": {"bar": {}, "serialization_only": {}}, "required": ["bar", "serialization_only"], "additionalProperties": False, }
if "anyOf" in schema: schema["oneOf"] = schema.pop("anyOf") OneOf = schema(extra=to_one_of) # or extra can be a dictionary which will update the schema @schema( extra={"$ref": "http://some-domain.org/path/to/schema.json#/$defs/Foo"}, override=True, # override apischema generated schema, using only extra ) @dataclass class Foo: bar: int # Use Annotated with OneOf to make a "strict" Union assert deserialization_schema(Annotated[Union[Foo, int], OneOf]) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "oneOf": [ # oneOf instead of anyOf { "$ref": "http://some-domain.org/path/to/schema.json#/$defs/Foo" }, { "type": "integer" }, ], }
from typing import NewType from apischema import schema_ref from apischema.json_schema import deserialization_schema Tags = NewType("Tags", Set[str]) schema_ref(...)(Tags) @dataclass class Resource: id: int tags: Tags assert deserialization_schema(Resource, all_refs=True) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "$defs": { "Resource": { "type": "object", "properties": { "id": { "type": "integer" }, "tags": { "$ref": "#/$defs/Tags" } }, "required": ["id", "tags"], "additionalProperties": False, },
Conversion(identity, source=Base, target=Base._union, inherited=False) ) @dataclass class Foo(Base): foo: int @dataclass class Bar(Base): bar: str assert ( deserialization_schema(Base) == serialization_schema(Base) == { "anyOf": [ { "type": "object", "properties": {"foo": {"type": "integer"}}, "required": ["foo"], "additionalProperties": False, }, { "type": "object", "properties": {"bar": {"type": "string"}}, "required": ["bar"], "additionalProperties": False, },
# Annotated can be used to add metadata to mapped fields def prefixed_foo(baz: str, pfx: Annotated[str, alias("prefix")] = "") -> Foo: return Foo(pfx + baz) wrapper, input_cls = dataclass_input_wrapper(prefixed_foo) assert wrapper(input_cls("oo", "f")) == prefixed_foo("oo", "f") == Foo("foo") # Used as conversion assert deserialize(Foo, { "baz": "oo", "prefix": "f" }, conversion=wrapper) == Foo("foo") assert deserialization_schema(Foo, conversion=wrapper) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "type": "object", "properties": { "baz": { "type": "string" }, "prefix": { "type": "string", "default": "" }, }, "required": ["baz"], "additionalProperties": False, }
deserializer(identity, Bar, Base) # Roughly equivalent to # @deserializer # def from_bar(bar: Bar) -> Base: # return bar # but identity is optimized by Apischema # You can even add deserializers which are not subclass @deserializer def from_list_of_int(ints: list[int]) -> Base: return Base() assert deserialization_schema(Base) == { "anyOf": [ { "type": "object", "properties": {"foo": {"type": "integer"}}, "required": ["foo"], "additionalProperties": False, }, { "type": "object", "properties": {"bar": {"type": "string"}}, "required": ["bar"], "additionalProperties": False, }, {"type": "array", "items": {"type": "integer"}}, ],
def make_schema(cls: type) -> Mapping[str, Any]: return deserialization_schema(cls, version=JsonSchemaVersion.DRAFT_7)
from dataclasses import dataclass, field from typing import Optional from pytest import raises from apischema import ValidationError, deserialize, serialize from apischema.json_schema import deserialization_schema, serialization_schema from apischema.metadata import none_as_undefined @dataclass class Foo: bar: Optional[str] = field(default=None, metadata=none_as_undefined) assert ( deserialization_schema(Foo) == serialization_schema(Foo) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "type": "object", "properties": {"bar": {"type": "string"}}, "additionalProperties": False, } ) with raises(ValidationError): deserialize(Foo, {"bar": None}) assert serialize(Foo, Foo(None)) == {}
@dataclass class Billing: name: str # Fields used in dependencies MUST be declared with `field` credit_card: NotNull[int] = field(default=None) billing_address: NotNull[str] = field(default=None) dependencies = dependent_required({credit_card: [billing_address]}) # it can also be done outside the class with # dependent_required({"credit_card": ["billing_address"]}, owner=Billing) assert deserialization_schema(Billing) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "additionalProperties": False, "dependentRequired": { "credit_card": ["billing_address"] }, "properties": { "name": { "type": "string" }, "credit_card": { "type": "integer" }, "billing_address": { "type": "string" },
@schema(pattern=r"^#[0-9a-fA-F]{6}$") @dataclass class RGB: red: int green: int blue: int @serializer @property def hexa(self) -> str: return f"#{self.red:02x}{self.green:02x}{self.blue:02x}" # serializer can also be called with methods/properties outside of the class # For example, `serializer(RGB.hexa)` would have the same effect as the decorator above @deserializer def from_hexa(hexa: str) -> RGB: return RGB(int(hexa[1:3], 16), int(hexa[3:5], 16), int(hexa[5:7], 16)) assert deserialize(RGB, "#000000") == RGB(0, 0, 0) assert serialize(RGB, RGB(0, 0, 42)) == "#00002a" assert (deserialization_schema(RGB) == serialization_schema(RGB) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", })
from apischema import deserialize, deserializer, type_name from apischema.json_schema import deserialization_schema from apischema.objects import object_deserialization def create_range(start: int, stop: int, step: int = 1) -> range: return range(start, stop, step) range_conv = object_deserialization(create_range, type_name("Range")) # Conversion can be registered deserializer(range_conv) assert deserialize(range, {"start": 0, "stop": 10}) == range(0, 10) assert deserialization_schema(range) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "type": "object", "properties": { "start": { "type": "integer" }, "stop": { "type": "integer" }, "step": { "type": "integer", "default": 1 }, }, "required": ["start", "stop"], "additionalProperties": False, }
from dataclasses import dataclass from typing import Optional from apischema.json_schema import deserialization_schema @dataclass class Node: value: int child: Optional["Node"] = None assert deserialization_schema(Node) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "$ref": "#/$defs/Node", "$defs": { "Node": { "type": "object", "properties": { "value": { "type": "integer" }, "child": { "anyOf": [{ "$ref": "#/$defs/Node" }, { "type": "null" }] }, }, "required": ["value"],
import os import time from datetime import datetime from apischema import deserialize, deserializer from apischema.json_schema import deserialization_schema # Set UTC timezone for example os.environ["TZ"] = "UTC" time.tzset() # There is already `deserializer(datetime.fromisoformat, str, datetime) in apischema # Let's add an other deserializer for datetime from a timestamp @deserializer def datetime_from_timestamp(timestamp: int) -> datetime: return datetime.fromtimestamp(timestamp) assert deserialization_schema(datetime) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "anyOf": [{"type": "string", "format": "date-time"}, {"type": "integer"}], } assert ( deserialize(datetime, "2019-10-13") == datetime(2019, 10, 13) == deserialize(datetime, 1570924800) )
def value(self, value: T): self._value = value deserializer( Recoverable, Union[T, Annotated[RecoverableRaw, Skip(schema_only=True)]], Recoverable[T], ) @serializer def serialize_recoverable(recoverable: Recoverable[T]) -> T: return recoverable.value assert deserialize(Recoverable[int], 0).value == 0 with raises(RecoverableRaw) as err: assert deserialize(Recoverable[int], "bad").value assert err.value.raw == "bad" assert serialize(Recoverable(0)) == 0 with raises(RecoverableRaw) as err: assert serialize(Recoverable(RecoverableRaw("bad"))) assert err.value.raw == "bad" assert (deserialization_schema(Recoverable[int]) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "type": "integer" } == serialization_schema(Recoverable[int]))
from apischema.json_schema import deserialization_schema @dataclass class Bar: baz: str @dataclass class Foo: bar1: Bar bar2: Bar assert deserialization_schema(Foo, all_refs=False) == { "$schema": "http://json-schema.org/draft/2019-09/schema#", "$defs": { "Bar": { "additionalProperties": False, "properties": { "baz": { "type": "string" } }, "required": ["baz"], "type": "object", } }, "additionalProperties": False, "properties": {