Сериализация: 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меняет формат конкретного поля только на выходе (деньги строкой, дата в своём формате), не трогая тип поля в модели.