Когда запрос приходит в сервис, это просто набор байт: строки, числа, 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 последовательно:
- Проверяет, что обязательные поля присутствуют.
- Приводит типы — например, строку
"42"к числу42, если поле ожидаетint. - Применяет ограничения из
Field. - Запускает пользовательские валидаторы.
Если что-то не так, собирает все ошибки и бросает исключение. FastAPI перехватывает его и формирует ответ 422 Unprocessable Entity.
Валидаторы для сложных правил
Field покрывает простые случаи: диапазон, длина, регулярное выражение. Когда нужно что-то сложнее — используют валидаторы.
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), а не общая для всех модель. Это та же логика, что и в роутинге: что входит и что выходит — разные контракты.
Коротко
- Модель Pydantic — класс-наследник
BaseModel; типы полей задаются аннотациями Python. Fieldдобавляет ограничения:min_length,gt,le,regexи другие.- Pydantic проверяет типы, приводит значения и собирает все ошибки перед тем, как бросить исключение.
field_validator— для одного поля;model_validator(mode="after")— для связей между полями.- Сериализация:
model_dump()→ словарь,model_dump_json()→ JSON; строить из словаря —model_validate(). - Разделяй модели по роли:
CreateProductRequestиProductResponse— разные классы с разным набором полей. - В новом коде не используй
.dict(),.json(),@validator,class Config— это Pydantic v1.
Что почитать дальше
- Роутинг и запросы в FastAPI — path operations, параметры, response_model.
- Async и конкурентность в FastAPI — как работает event loop и когда нужен
async def.