Когда запрос приходит в сервис, это просто набор байт: строки, числа, JSON. Всё это нужно превратить в конкретные объекты с понятными типами — и убедиться, что данные вообще корректны. Руками такую проверку писать неудобно и легко что-то пропустить. FastAPI решает эту задачу через Pydantic: описываешь структуру данных один раз, и всё остальное происходит автоматически.
Здесь речь о Pydantic v2. У второй версии другой API по сравнению с первой. Старые @validator и class Config — это v1; в новом коде их не используют.
Модель — это описание структуры данных
Раньше для проверки входных данных писали что-то вроде:
def create_product(data: dict):
if "name" not in data or not isinstance(data["name"], str):
raise ValueError("bad name")
if "price" not in data or data["price"] <= 0:
raise ValueError("bad price")
...
Такой код разрастается, дублируется и всё равно что-то упускает.
Pydantic предлагает другой подход: описать структуру в виде класса-наследника BaseModel, а проверку выполнит библиотека.
from pydantic import BaseModel, Field
class CreateProductRequest(BaseModel):
name: str = Field(min_length=1, max_length=200)
price: int = Field(gt=0)
category: str
Этого уже достаточно. FastAPI разберёт тело запроса в CreateProductRequest, проверит длину имени и положительность цены, а при нарушении вернёт 422 с описанием, какое именно поле не прошло проверку — не доводя дело до вашего кода.
Типы полей — обычные аннотации Python. Field добавляет ограничения (min_length, gt, le) и метаданные (например, description для автодокументации).
Как Pydantic проверяет данные
При создании модели Pydantic последовательно:
Ошибки по полям собираются все сразу и уходят одним 422; валидатор модели after не вызывается, если упало хотя бы одно поле.
- Проверяет, что обязательные поля присутствуют.
- Приводит типы — например, строку
"42"к числу42, если поле ожидаетint. - Применяет ограничения из
Field. - Запускает пользовательские валидаторы.
Если что-то не так, собирает все ошибки и бросает исключение. FastAPI перехватывает его и формирует ответ 422 Unprocessable Entity.
Валидаторы для сложных правил
Field покрывает простые случаи: диапазон, длина, регулярное выражение (pattern=; аргумент regex остался в первой версии и теперь даёт ошибку). Когда нужно что-то сложнее — используют валидаторы.
field_validator проверяет и нормализует одно поле:
from pydantic import BaseModel, field_validator
class CreateProductRequest(BaseModel):
name: str
@field_validator("name")
@classmethod
def strip_name(cls, value: str) -> str:
return value.strip()
model_validator получает доступ ко всей модели сразу — удобен для проверки связей между полями:
from pydantic import BaseModel, model_validator
class CreateProductRequest(BaseModel):
price: int
discount_price: int | None = None
@model_validator(mode="after")
def check_discount(self):
if self.discount_price is not None and self.discount_price >= self.price:
raise ValueError("discount_price must be lower than price")
return self
mode="after" означает, что валидатор запускается после того, как модель уже собрана и все поля присвоены. Брошенный ValueError FastAPI превращает в понятную ошибку с указанием поля.
Сериализация — обратная задача
Валидация — это вход: JSON → объект. Сериализация — выход: объект → JSON или словарь.
В Pydantic v2 для этого есть два метода:
product = ProductResponse(id=1, name="Кофемолка", price=4990)
product.model_dump() # {"id": 1, "name": "Кофемолка", "price": 4990}
product.model_dump_json() # '{"id":1,"name":"Кофемолка","price":4990}'
Старые .dict() и .json() остались от первой версии — в новом коде их не используют.
Строить модель из словаря можно через model_validate:
data = {"id": 1, "name": "Кофемолка", "price": 4990}
product = ProductResponse.model_validate(data)
В эндпоинтах сериализацию обычно делает сам FastAPI через параметр response_model — вручную вызывать model_dump нужно реже, в основном для логов или формирования данных для внешних вызовов.
Почему важно разделять модели запроса и ответа
Соблазн — одна модель и на вход, и на выход:
class Product(BaseModel):
id: int
name: str
price: int
created_at: datetime
Проблема: если использовать её и для запроса, клиент может прислать id или created_at, которые он не должен задавать. Если использовать для ответа, придётся вернуть все поля — в том числе, возможно, лишние.
Правильный подход — разные модели для разных ролей:
class CreateProductRequest(BaseModel):
name: str
price: int
class ProductResponse(BaseModel):
id: int
name: str
price: int
created_at: datetime
Запрос содержит только то, что клиент вправе передать. Ответ — только то, что сервис готов показать. Преобразование между ними — явная функция (from_domain, to_response), а не общая для всех модель. Это та же логика, что и в роутинге: что входит и что выходит — разные контракты.
Глубже: from_attributes, strict, extra и псевдонимырасширенное
Ответ обычно собирают не из словаря, а из ORM-объекта. Чтобы ProductResponse.model_validate(product)
прочитал поля по атрибутам, модели включают from_attributes:
from pydantic import BaseModel, ConfigDict
class ProductResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
По умолчанию лишние поля во входных данных молча отбрасываются: опечатка клиента в имени
поля не станет ошибкой, а просто потеряется. Для моделей запроса стоит включить
ConfigDict(extra="forbid") — тогда неизвестное поле даст 422 с понятной подсказкой.
Приведение типов тоже можно выключить: в строгом режиме (strict=True в ConfigDict или
при вызове model_validate) строка "42" в поле int — ошибка, а не число. Это уместно,
когда данные приходят из своего же кода и приведение скрывало бы баг.
Имя поля в JSON и в Python могут отличаться: user_name: str = Field(alias="userName")
принимает userName на входе, а model_dump(by_alias=True) отдаёт его обратно в том же
виде. Поле, которое считается из других, помечают @computed_field — оно попадёт в
сериализацию и в схему. А когда проверить нужно не модель, а просто список чисел из
внешнего ответа, берут TypeAdapter(list[int]).validate_python(data).
Коротко
- Модель Pydantic — класс-наследник
BaseModel; типы полей задаются аннотациями Python. Fieldдобавляет ограничения:min_length,gt,le,patternи другие.- Pydantic проверяет типы, приводит значения и собирает все ошибки перед тем, как бросить исключение.
field_validator— для одного поля;model_validator(mode="after")— для связей между полями.- Сериализация:
model_dump()→ словарь,model_dump_json()→ JSON; строить из словаря —model_validate(). - Разделяй модели по роли:
CreateProductRequestиProductResponse— разные классы с разным набором полей. - В новом коде не используй
.dict(),.json(),@validator,class Config— это Pydantic v1. from_attributesсобирает ответ из ORM-объекта,extra="forbid"ловит опечатки клиента,strictвыключает приведение типов,aliasиby_aliasразводят имена в JSON и в Python.
Что пощупать
Две границы проверки в одном сервисе практикума remodov/marketplace-system-python: формат запроса ловят модели pydantic (Field(gt=0) у цены, обязательная delta), а смысл (ноль как изменение остатка, скидка глубже половины) проверяет класс Product. Типы ошибок pydantic переводятся в русские сообщения по полям, и ответ - 400, а не 422.
Код: router.py, problem.py, model.py.
Сделаем сами
Ветка step-03-commands-and-errors.
Что почитать дальше
- Роутинг и запросы в FastAPI — path operations, параметры, response_model.
- Async и конкурентность в FastAPI — как работает event loop и когда нужен
async def.