← назад к разделу

Когда запрос приходит в сервис, это просто набор байт: строки, числа, 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 последовательно:

  1. Проверяет, что обязательные поля присутствуют.
  2. Приводит типы — например, строку "42" к числу 42, если поле ожидает int.
  3. Применяет ограничения из Field.
  4. Запускает пользовательские валидаторы.

Если что-то не так, собирает все ошибки и бросает исключение. 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.