Pydantic supports annotating third-party types so they can be used directly in Pydantic models and de/serialized to & from JSON.
For example:
from typing import Annotated, Any
from pydantic import BaseModel, model_validator
from pydantic.functional_validators import ModelWrapValidatorHandler
from typing_extensions import Self
# Pretend this is some third-party class
# we can't modify directly...
class Quantity:
def __init__(self, value: float, unit: str):
self.value = value
self.unit = unit
class QuantityAnnotations(BaseModel):
value: float
unit: str
@model_validator(mode="wrap")
def _validate(value: Any, handler: ModelWrapValidatorHandler[Self]) -> Quantity:
if isinstance(value, Quantity):
return value
validated = handler(value)
if isinstance(validated, Quantity):
return validated
return Quantity(**dict(validated))
QuantityType = Annotated[Quantity, QuantityAnnotations]
class OurModel(BaseModel):
quantity: QuantityType = Quantity(value=0.0, unit='m')
This works fine, because we just annotated the Quantity
type so Pydantic knows how to serialize it to JSON with no issues:
model_instance = OurModel()
print(model_instance.model_dump_json())
# {"quantity":{"value":0.0,"unit":"m"}}
But if we instead try to get the JSON Schema that describes OurModel
, we get a warning that it doesn't know how to serialize the default value (the one it just successfully serialized)...
OurModel.model_json_schema()
# ...lib/python3.10/site-packages/pydantic/json_schema.py:2158: PydanticJsonSchemaWarning:
# Default value <__main__.Quantity object at 0x75fcccab1960> is not JSON serializable;
# excluding default from JSON schema [non-serializable-default]
What am I missing here? Is this a Pydantic bug, or do I need to add more to the annotations to tell Pydantic how to serialize the default value in the context of a JSON Schema?
Does anyone have a good workaround to easily include annotated third-party types as default values in JSON Schema generated by Pydantic?
You can provide multiple annotations to Pydantic to help with schema generation, etc.
Below is a full example:
class _QuantityJson(BaseModel):
value: float
unit: str
def _validate_quantity(value: Any) -> Quantity:
if isinstance(value, Quantity):
return value
if isinstance(value, dict):
_qjson = _QuantityJson.model_validate(value)
return Quantity(value=_qjson.value, unit=_qjson.unit)
raise ValueError(f"cannot parse quantity from {value}")
_schema_quantity = _QuantityJson.model_json_schema() | {"title": "Quantity"}
def _serialize_quantity(quantity: Quantity):
return _QuantityJson(value=quantity.value, unit=quantity.unit)
QuantityAnnotated: TypeAlias = Annotated[
Quantity,
Field(validate_default=True),
PlainValidator(_validate_quantity),
WithJsonSchema(_schema_quantity),
PlainSerializer(_serialize_quantity),
]
Then you can use it in your model:
class Model(BaseModel):
quantity: QuantityAnnotated = Field(default=_QuantityJson(value=0.0, unit="m"))
You need to use a _QuantityJson
default value because the serializer is not used for the default value (maybe a Pydantic bug?). You can directly assign the default value, but any decent type checker will throw a warning, so I am using a Field
with default.
Below is an example usage:
print(Model.model_json_schema())
print(Model.model_validate(dict(quantity=Quantity(3.2, "m"))))
print(Model.model_validate(dict(quantity=Quantity(3.2, "m"))).model_dump_json())
print(Model.model_validate(dict(quantity=dict(value=3.2, unit="m"))))
print(Model.model_validate_json(b"""{"quantity": {"value": 3.3, "unit": "m"}}"""))