With Pydantic 2.13.4, a model containing a reference-style nested BaseModel passes model_validate(), but model_dump() raises PydanticSerializationError: Error calling function serialize: AttributeEr
A wrap model validator can bypass its handler and return a value that violates the declared type. Validation then succeeds, but serialization still uses the schema for the declared nested model. In Pydantic 2.13.4, returning None for a nonnullable model field makes the nested model_serializer run with None as its self argument.
If None is a legitimate state, declare the parent field as Reference | None. The nullable serialization branch then emits JSON null without invoking the reference serializer. Do not patch the serializer to tolerate an impossible self value. If the field really is required, pass None to the wrap validator's handler so normal validation rejects it instead of returning it unchanged.
Runnable reproduction and fix (verified on Pydantic 2.13.4):
from pydantic import BaseModel, model_serializer, model_validator
class Reference(BaseModel):
target: str
@model_validator(mode='wrap')
@classmethod
def resolve(cls, value, handler):
return None if value is None else handler(value)
@model_serializer
def serialize(self):
return {'target': self.target}
class RequiredReference(BaseModel):
reference: Reference
required = RequiredReference.model_validate({'reference': None})
assert required.reference is None # validation accepted the bypass
try:
required.model_dump()
except Exception as error:
print(type(error).__name__, error)
# PydanticSerializationError: ... NoneType ... target
class OptionalReference(BaseModel):
reference: Reference | None
optional = OptionalReference.model_validate({'reference': None})
assert optional.model_dump() == {'reference': None}Audit the parent field annotation and wrap-validator return path when a nested serializer unexpectedly receives None. Successful model validation alone does not establish that a custom wrap validator returned an instance of its annotated model.