Сериализация: model_dump, computed_field, алиасы

Сериализация — это превращение модели обратно в словарь или JSON: model_dump решает, какие поля попадут наружу, под какими именами и в каком виде.

Сериализация в Pydantic — это процесс получения из объекта модели простого представления (dict или JSON-строки) для ответа API, логов или сохранения. Главные инструменты — методы model_dump() и model_dump_json().

Валидация отвечает за «вход» (данные пришли — проверили), сериализация — за «выход» (отдаём данные наружу). И тут возникает много тонких решений: показывать ли поле password в ответе (нет!), как назвать ключи для фронтенда (camelCase вместо snake_case), как отдать дату (ISO-строкой или числом), нужно ли добавить вычисляемое поле full_name, которого нет в модели как атрибута. Всем этим управляет именно слой сериализации.

Этот урок — про model_dump и его параметры (by_alias, exclude, include, exclude_none), про вычисляемые поля computed_field и про кастомные сериализаторы, когда нужно поменять формат конкретного поля на выходе.

Зачем это на практике

Грамотная сериализация — это и безопасность, и удобство клиента:

  • Прятать секреты: password_hash, внутренние флаги, токены не должны утекать в ответ — их исключают из дампа.
  • Имена под фронтенд: JS-клиенты ждут userId, а в Python принят user_id — алиасы решают это без переименования полей.
  • Производные значения: клиенту удобнее готовое full_name или total_price, чем собирать их самому — это и есть computed_field.
  • Форматы на выходе: дату отдать как ISO-строку, деньги — строкой с двумя знаками, enum — его значением. Кастомные сериализаторы задают это точечно.

Код с from pydantic import ... в браузере не запустится (кнопки «Запустить» нет) — читайте его как образец. Базовые идеи (переименование ключей, вычисляемое поле) ниже продублированы на чистом Python, который исполняется.

model_dump и model_dump_json

Два основных метода: model_dump() возвращает Python-dict (значения остаются объектами Python — например, datetime останется datetime), а model_dump_json() сразу возвращает JSON-строку (там datetime станет строкой, потому что JSON не знает дат).

from datetime import date
from pydantic import BaseModel

class User(BaseModel):
    name: str
    born: date

u = User(name="Анна", born=date(2000, 5, 1))

print(u.model_dump())
# {'name': 'Анна', 'born': datetime.date(2000, 5, 1)}

print(u.model_dump_json())
# {"name":"Анна","born":"2000-05-01"}

Разница принципиальна: model_dump() удобен, когда результат пойдёт в другой Python-код, а model_dump_json() — когда нужен готовый JSON для HTTP-ответа. FastAPI при возврате модели делает по сути model_dump сам, но знать параметры полезно: их можно прокинуть через response_model и настройки эндпоинта.

exclude, include, exclude_none

Чем управлять составом полей в дампе:

ПараметрЧто делает
exclude={"password"}убрать перечисленные поля из результата
include={"id", "name"}оставить только перечисленные поля
exclude_none=Trueвыкинуть все поля, у которых значение None
exclude_unset=Trueоставить только поля, явно заданные при создании (не дефолты)
from pydantic import BaseModel

class Account(BaseModel):
    id: int
    name: str
    password: str
    note: str | None = None

a = Account(id=1, name="Анна", password="secret")

print(a.model_dump(exclude={"password"}))
# {'id': 1, 'name': 'Анна', 'note': None}

print(a.model_dump(exclude={"password"}, exclude_none=True))
# {'id': 1, 'name': 'Анна'}

Связка exclude + exclude_none даёт компактный ответ без секретов и без пустых полей. А exclude_unset=True особенно полезен в PATCH-эндпоинтах: так в дамп попадают только реально присланные поля, и вы обновляете именно их, не затирая остальное дефолтами.

Алиасы: имена для внешнего мира

Алиас — это альтернативное имя поля для ввода/вывода. Классический случай: внутри Python поле зовётся user_id, а наружу должно выходить как userId. Задают алиас через Field(alias=...), а при дампе включают by_alias=True.

from pydantic import BaseModel, Field

class Profile(BaseModel):
    user_id: int = Field(alias="userId")
    full_name: str = Field(alias="fullName")

p = Profile(userId=7, fullName="Анна")  # вход — по алиасу

print(p.model_dump(by_alias=True))
# {'userId': 7, 'fullName': 'Анна'}
print(p.model_dump())
# {'user_id': 7, 'full_name': 'Анна'}

Без by_alias=True дамп идёт по «питоновским» именам полей. Чтобы не выписывать алиас каждому полю вручную, в model_config задают alias_generator (например, функцию to_camel), и тогда все поля автоматически получают camelCase-алиасы. Сама идея переименования ключей — это просто отображение «имя поля → имя на выходе»; вот она на чистом Python:

data  = {"user_id": 7, "full_name": "Анна"}
alias = {"user_id": "userId", "full_name": "fullName"}

print("by_field:", data)
out = {alias[k]: v for k, v in data.items()}
print("by_alias:", out)

Вывод:

by_field: {'user_id': 7, 'full_name': 'Анна'}
by_alias: {'userId': 7, 'fullName': 'Анна'}

computed_field: вычисляемые поля в выводе

Иногда наружу нужно отдать значение, которого нет среди обычных полей: full_name из имени и фамилии, area из ширины и высоты, is_adult из даты рождения. Делается это через свойство, помеченное @computed_field поверх @property. Такое поле появляется в дампе, хотя не принимается на входе.

from pydantic import BaseModel, computed_field

class Rect(BaseModel):
    width: float
    height: float

    @computed_field
    @property
    def area(self) -> float:
        return self.width * self.height

r = Rect(width=4, height=5)
print(r.model_dump())
# {'width': 4.0, 'height': 5.0, 'area': 20.0}

Идея «обычные поля плюс вычисляемое» наглядна и на чистом Python через обычное @property:

class Rect:
    def __init__(self, w, h):
        self.w, self.h = w, h
    @property
    def area(self):
        return self.w * self.h

r = Rect(4, 5)
print("area:", r.area)
print({"w": r.w, "h": r.h, "area": r.area})

Вывод:

area: 20
{'w': 4, 'h': 5, 'area': 20}

Разница лишь в том, что computed_field учит Pydantic включать area в model_dump() и в JSON-схему ответа, а обычное @property в дамп бы не попало.

Кастомные сериализаторы

Когда формат конкретного поля на выходе нужно изменить, используют field_serializer. Типичный пример — отдавать сумму денег строкой с двумя знаками после запятой, а не числом с плавающей точкой:

from pydantic import BaseModel, field_serializer

class Order(BaseModel):
    total: float

    @field_serializer("total")
    def serialize_total(self, v: float) -> str:
        return f"{v:.2f}"

print(Order(total=19.5).model_dump())
# {'total': '19.50'}

Это удобно для денег (избегаем артефактов float вроде 19.499999), для дат в нестандартном формате, для enum, который хочется отдать человекочитаемой строкой. Сериализатор влияет только на вывод — само поле в модели остаётся float.

Как это работает под капотом

Как и валидация, сериализация в Pydantic v2 описывается единой core schema и исполняется скомпилированным ядром pydantic-core на Rust. Для каждого поля в схеме хранится «сериализатор» — правило, как превратить значение в выходное представление; ваши field_serializer и computed_field встраиваются в это правило. Поэтому model_dump() работает быстро даже на больших вложенных структурах.

Полезно различать два «режима» вывода. У дампа есть параметр mode: model_dump(mode="python") (по умолчанию) оставляет значения Python-объектами (datetime, Decimal), а model_dump(mode="json") приводит их к JSON-совместимым типам (строки, числа), но возвращает всё ещё dict, а не строку. Метод model_dump_json() — это, по сути, mode="json" плюс финальная упаковка в строку. Зная это, легко выбрать нужный вид результата под задачу.

Частые ошибки

  • Забыли by_alias=True. Алиасы заданы, а дамп всё равно идёт по питоновским именам — потому что без by_alias=True алиасы на вывод не применяются.
  • Секрет утёк в ответ. Поле password/token не исключили — и оно ушло клиенту. Исключайте такие поля (exclude) или не кладите их в модель ответа вовсе.
  • computed_field без @property. Декоратор @computed_field ставится поверх @property; без него поле в дамп не попадёт.
  • Ждут JSON от model_dump(). model_dump() возвращает dict с Python-объектами (дата останется date). Для готовой JSON-строки нужен model_dump_json().
  • Путают exclude_none и exclude_unset. Первый убирает поля со значением None, второй — поля, которые не задавали явно (остались дефолтом). В PATCH обычно нужен именно exclude_unset.

Итоги

  • model_dump() отдаёт dict с Python-объектами, model_dump_json() — готовую JSON-строку (даты и прочее уже приведены).
  • Состав полей регулируют exclude/include, exclude_none, exclude_unset; последний особенно полезен для PATCH.
  • Алиасы (Field(alias=...)) дают внешние имена ключей; на выводе их включает by_alias=True, а alias_generator расставляет их массово.
  • computed_field поверх @property добавляет в дамп производное поле, которого нет среди входных атрибутов.
  • field_serializer меняет формат конкретного поля только на выходе (деньги строкой, дата в своём формате), не трогая тип поля в модели.
Проверьте себя
1. В чём разница между model_dump() и model_dump_json()?
Amodel_dump() возвращает dict с Python-объектами (дата останется date), model_dump_json() — готовую JSON-строку, где дата уже стала строкой
Bmodel_dump() работает только в FastAPI, а model_dump_json() — в любом коде
CРазницы нет, это псевдонимы одного метода
Dmodel_dump() шифрует данные, а model_dump_json() — нет
2. Алиасы заданы через Field(alias="userId"), но model_dump() выдаёт ключ user_id. Почему?
AБез by_alias=True дамп идёт по питоновским именам полей; алиасы применяются на вывод только при by_alias=True
BАлиасы в Pydantic v2 не поддерживаются
CНужно переименовать само поле в userId
DЭто баг, обойти нельзя
3. Как добавить в вывод поле full_name, которого нет среди входных атрибутов модели?
AОбъявить свойство с @computed_field поверх @property — оно попадёт в model_dump(), хотя на входе не принимается
BПросто добавить full_name: str — Pydantic вычислит его сам
CЭто невозможно: в дамп попадают только обычные поля
DПередать его в exclude