Pydantic support¶
It takes only 30 lines of code to support pydantic.BaseModel
and all of its subclasses. You could add these lines to your project using pydantic and start to benefit from apischema features.
This example deliberately doesn't use set_object_fields
but instead the conversions feature in order to roughly include pydantic "as is": it will reuse pydantic coercion, error messages, JSON schema, etc. This makes a full retro-compatible support.
As a result, lot of apischema features like GraphQL schema generation or NewType
validation cannot be supported using this method — but they could be by using set_object_fields
instead.
import inspect
from collections.abc import Mapping
from typing import Any
import pydantic
import pytest
from apischema import (
ValidationError,
deserialize,
schema,
serialize,
serializer,
settings,
)
from apischema.conversions import AnyConversion, Conversion
from apischema.json_schema import deserialization_schema
from apischema.schemas import Schema
# ---------- Pydantic support code starts here ----------
prev_deserialization = settings.deserialization.default_conversion
def default_deserialization(tp: Any) -> AnyConversion | None:
if inspect.isclass(tp) and issubclass(tp, pydantic.BaseModel):
def deserialize_pydantic(data):
try:
return tp.parse_obj(data)
except pydantic.ValidationError as error:
raise ValidationError.from_errors(
[{"loc": err["loc"], "err": err["msg"]} for err in error.errors()]
)
return Conversion(
deserialize_pydantic,
source=tp.__annotations__.get("__root__", Mapping[str, Any]),
target=tp,
)
else:
return prev_deserialization(tp)
settings.deserialization.default_conversion = default_deserialization
prev_schema = settings.base_schema.type
def default_schema(tp: Any) -> Schema | None:
if inspect.isclass(tp) and issubclass(tp, pydantic.BaseModel):
return schema(extra=tp.schema(), override=True)
else:
return prev_schema(tp)
settings.base_schema.type = default_schema
# No need to use settings.serialization because serializer is inherited
@serializer
def serialize_pydantic(obj: pydantic.BaseModel) -> Any:
# There is currently no way to retrieve `serialize` parameters inside converters,
# so exclude_unset is set to True as it's the default apischema setting
return getattr(obj, "__root__", obj.dict(exclude_unset=True))
# ---------- Pydantic support code ends here ----------
class Foo(pydantic.BaseModel):
bar: int
assert deserialize(Foo, {"bar": 0}) == Foo(bar=0)
assert serialize(Foo, Foo(bar=0)) == {"bar": 0}
assert deserialization_schema(Foo) == {
"$schema": "http://json-schema.org/draft/2020-12/schema#",
"title": "Foo", # pydantic title
"type": "object",
"properties": {"bar": {"title": "Bar", "type": "integer"}},
"required": ["bar"],
}
with pytest.raises(ValidationError) as err:
deserialize(Foo, {"bar": "not an int"})
assert err.value.errors == [
{"loc": ["bar"], "err": "value is not a valid integer"} # pydantic error message
]