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

PYTHON · FASTAPI Фаза 5. Фундамент - ядро FastAPI

Обязательно

Pydantic v2: валидация и сериализация

BaseModel · Validators · Schemas

Входные байты превращаются в проверенные объекты, а публичные модели ограничивают, что сервис отдаёт обратно.

1. BaseModel описывает контракт данных

1 / 5

Аннотации задают типы, Field - длину, диапазон и pattern. Ошибки по всем полям собираются вместе и превращаются в 422 до вызова обработчика.

JSONname: ""price: -10CreateProductRequestname: min_length=1price: gt=0category: str4222 errors

2. Валидация идёт по понятным стадиям

2 / 5

Сначала обязательные поля, затем типы и ограничения, после - пользовательские правила. model_validator(after) видит уже собранную модель.

Requiredполя на местеTypes"42" → 42Fieldgt · patternValidatorsfield + model

3. Валидаторы нормализуют и связывают поля

3 / 5

field_validator очищает одно значение, model_validator проверяет инвариант: скидка меньше цены, дата конца позже даты начала.

field_validator("name")" Кофе "→ "Кофе"model_validator(after)discount_price < pricereturn self

4. model_validate входит, model_dump выходит

4 / 5

model_validate создаёт объект из словаря или ORM, model_dump возвращает dict, model_dump_json - JSON. Методы dict и json относятся к v1.

dict / ORMProductResponsefrom_attributesmodel_validatemodel_dump(_json)

5. Запрос и ответ - разные модели

5 / 5

Клиент задаёт name и price, сервер сам создаёт id и created_at. extra=forbid ловит опечатки, strict отключает приведение, alias связывает userName и user_name.

CreateProductRequestname · priceextra="forbid"ProductResponseid · name · pricecreated_atdomain

Когда запрос приходит в сервис, это просто набор байт: строки, числа, 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 ограничения Field gt, min_length, pattern валидаторы field и model

Ошибки по полям собираются все сразу и уходят одним 422; валидатор модели after не вызывается, если упало хотя бы одно поле.

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

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

Что почитать дальше