Search code examples
pythonjsonjsonschemapydanticpydantic-v2

How to fix Pydantic "Default value is not JSON serializable" warning when using third-party annotated type? [non-serializable-default]


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?


Solution

  • 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"}}"""))