Как написать кастомные сериализаторы с помощью @model_serializer в Pydantic v2?
@model_serializer заменяет стандартную сериализацию всей модели целиком: в режиме plain метод полностью контролирует выходной словарь, в режиме wrap — получает handler для вызова стандартной логики и может её модифицировать.
Кастомные сериализаторы с @model_serializer в Pydantic v2
Декоратор @model_serializer позволяет полностью контролировать, как вся модель целиком превращается в сериализованное представление. В отличие от @field_serializer, который работает с одним полем, @model_serializer получает доступ ко всему экземпляру и возвращает итоговое значение — обычно словарь или примитив.
Базовый синтаксис
from pydantic import BaseModel, model_serializer
class Money(BaseModel):
amount: int # в копейках
currency: str
@model_serializer
def serialize(self) -> dict:
return {
"amount": self.amount / 100,
"currency": self.currency.upper(),
"display": f"{self.amount / 100:.2f} {self.currency.upper()}",
}
m = Money(amount=9999, currency="rub")
print(m.model_dump())
# {'amount': 99.99, 'currency': 'RUB', 'display': '99.99 RUB'}
print(m.model_dump_json())
# {"amount":99.99,"currency":"RUB","display":"99.99 RUB"}
Параметр mode: 'plain' vs 'wrap'
mode='plain' (по умолчанию) — ваш метод полностью заменяет стандартную сериализацию. mode='wrap' — ваш метод получает вторым аргументом handler и может вызвать стандартную сериализацию, а затем модифицировать результат.
from pydantic import BaseModel, model_serializer, SerializerFunctionWrapHandler
from typing import Any
class AuditedModel(BaseModel):
name: str
secret: str
@model_serializer(mode="wrap")
def serialize(self, handler: SerializerFunctionWrapHandler) -> dict[str, Any]:
result = handler(self) # стандартный словарь со всеми полями
result.pop("secret", None) # убираем чувствительное поле
result["_serialized"] = True
return result
obj = AuditedModel(name="Alice", secret="s3cr3t")
print(obj.model_dump())
# {'name': 'Alice', '_serialized': True} — secret удалён
Учёт mode сериализации (python vs json)
Если модель сериализуется через model_dump(mode='json') или model_dump_json(), вы можете узнать об этом через параметр info:
from pydantic import BaseModel, model_serializer, SerializationInfo
from datetime import datetime
class Event(BaseModel):
name: str
ts: datetime
@model_serializer(mode="wrap")
def serialize(self, handler, info: SerializationInfo) -> dict:
result = handler(self)
if info.mode == "json":
result["ts"] = result["ts"] # уже строка в json mode
else:
result["ts_epoch"] = self.ts.timestamp() # добавляем epoch только для Python mode
return result
e = Event(name="Deploy", ts=datetime(2024, 1, 15, 12, 0, 0))
print(e.model_dump()) # содержит ts_epoch
print(e.model_dump(mode="json")) # не содержит ts_epoch
return_as_any — произвольный тип возврата
По умолчанию Pydantic ожидает, что сериализатор вернёт словарь. Если нужно вернуть строку или другой примитив, используйте return_as_any=True:
from pydantic import BaseModel, model_serializer
class Tag(BaseModel):
name: str
color: str
@model_serializer(return_as_any=True)
def serialize(self) -> str:
return f"#{self.color}:{self.name}"
tag = Tag(name="urgent", color="red")
print(tag.model_dump()) # '#red:urgent'
Подводные камни
- @model_serializer перезаписывает ВСЁ — в режиме
plainстандартная логика полностью заменяется. Если забыть вернуть поля явно, они пропадут из вывода без ошибки. - include/exclude не работают —
model_dump(include={'name'})игнорируется, если есть кастомный сериализатор в режиме plain; фильтрацию нужно реализовывать вручную. - model_dump_json() вызывает сериализатор тоже — убедитесь, что возвращаемые типы JSON-сериализуемы, иначе получите
PydanticSerializationError. - Рекурсия во wrap-режиме — не вызывайте
self.model_dump()внутри сериализатора; это вызовет бесконечную рекурсию. Используйтеhandler(self). - Совместимость с model_copy() — кастомный сериализатор не влияет на
model_copy(), только наmodel_dump()иmodel_dump_json(). - Нет доступа к context через self — контекст передаётся через
info.contextв параметреSerializationInfo, а не через атрибуты модели. - Унаследованные модели —
@model_serializerна родительском классе применяется и к потомкам, что может дать неожиданный результат, если потомок добавил новые поля.
Common mistakes
- Описывать model serializers только как термин и не показывать механизм на минимальном примере.
- Игнорировать ошибки, пустые данные, конкурентный доступ или границы транзакции.
- Не связывать поведение с официальным контрактом Pydantic и реальной эксплуатацией.
What the interviewer is testing
- Объясняет model serializers через последовательность действий, а не через набор ключевых слов.
- Приводит короткий кодовый пример или production-сценарий с ожидаемым поведением.
- Называет хотя бы один риск: производительность, безопасность, транзакции, память или сопровождение.