PydanticMiddleCoding

Как написать кастомные сериализаторы с помощью @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-сценарий с ожидаемым поведением.
  • Называет хотя бы один риск: производительность, безопасность, транзакции, память или сопровождение.

Sources

Related topics