Продвинутый курс по Pydantic

Курс для разработчиков, которые уже используют Pydantic и хотят глубже разобраться в моделях, валидации, настройке схем и производительности. Разберём архитектурные паттерны, интеграции с FastAPI/ORM, миграции между версиями и практики построения устойчивых доменных моделей.

1. Архитектура моделей и строгая типизация

Архитектура моделей и строгая типизация

Pydantic решает две задачи одновременно:

  • валидация и преобразование входных данных (часто из JSON, форм, событий очередей)
  • типизация и контракт между частями системы (модели как документация и граница ответственности)
  • В продвинутых проектах основная сложность — не написать ещё один BaseModel, а выстроить архитектуру моделей, где:

  • границы между слоями системы явно выражены
  • типы отражают доменную логику, а не “как пришло из API”
  • конвертация и валидация централизованы и предсказуемы
  • строгость контролируется осознанно, а не случайно
  • > В этой статье мы будем опираться на Pydantic v2 (актуальная ветка). Документация: Pydantic Documentation.

    !Поток данных через слои моделей и места, где применяется строгая типизация

    Модель как контракт и как граница

    В архитектуре модели выполняют роль границ:

  • между внешним миром и приложением (HTTP, очереди, файлы)
  • между сервисами внутри системы
  • между доменной логикой и хранением данных
  • Если смешать эти границы в одной модели, появляются типичные проблемы:

  • входные “грязные” данные начинают протекать в домен
  • доменные правила превращаются в набор валидаторов “на всё”
  • изменения API начинают ломать внутреннюю логику
  • Практический вывод: делайте несколько типов моделей под разные слои.

    Слои моделей: transport, domain, persistence

    Одна из рабочих стратегий — разделять модели по назначению.

    Transport-модели (DTO)

    Transport-модели описывают то, как данные приходят/уходят.

  • допускают необязательные поля, разные форматы, алиасы
  • выполняют нормализацию (например, trimming строк)
  • не должны знать про сложные доменные инварианты, которые завязаны на бизнес-правила
  • Пример:

    Domain-модели

    Domain-модели — то, с чем живёт бизнес-логика. Часто они:

  • более строгие
  • используют осмысленные типы вместо “просто str/int
  • по возможности не зависят от внешних алиасов и форматов
  • В некоторых командах доменные сущности делают не на Pydantic (например, dataclasses), а Pydantic используют как “периметр”. Но Pydantic v2 позволяет комфортно строить строгие доменные модели тоже — вопрос архитектурного выбора.

    Persistence-модели

    Persistence-модели описывают то, как объект хранится в БД/кэше/индексе.

  • поля могут отличаться по именам и структуре
  • могут добавляться служебные поля (created_at, version, deleted)
  • часто имеют требования к сериализации
  • Управление строгостью: где быть строгим, а где гибким

    Строгая типизация в Pydantic — это контроль над тем, допускаете ли вы неявные преобразования. Пример неявного преобразования: строка "123" превращается в int(123).

    Ключевая идея: строгость — это архитектурное решение по слоям.

  • На входе (transport) иногда полезна мягкость, чтобы принимать разные клиентские форматы.
  • В домене и на границах критичных подсистем чаще нужна строгость, чтобы ошибки всплывали рано.
  • В Pydantic v2 строгость обычно задают через конфигурацию и/или строгие типы.

    Документация по конфигурации: Model Config.

    Конфигурация модели: единые правила на весь класс

    В v2 конфигурация задаётся через model_config = ConfigDict(...).

    Наиболее архитектурно значимые параметры:

  • extra='forbid' | 'ignore' | 'allow'
  • strict=True (сделать модель строгой по умолчанию)
  • populate_by_name=True (разрешить заполнение по имени поля, а не только по алиасу)
  • Пример строгой доменной модели:

    Что даёт strict=True:

  • меньше “магии” преобразований
  • меньше скрытых ошибок интеграции
  • более предсказуемый контракт между командами и сервисами
  • Строгие типы: точечная строгость вместо глобальной

    Иногда не нужна строгость всего, а нужна строгость на ключевых полях. Для этого есть строгие типы.

    В Pydantic доступны, например:

  • StrictInt, StrictStr, StrictBool, StrictFloat
  • Документация: Strict Types.

    Пример:

    Архитектурный смысл: вы можете оставить transport-модель достаточно гибкой, но критичные параметры (страницы, лимиты, флаги доступа) сделать строгими.

    Annotated и Field: тип — отдельно, ограничения — отдельно

    В продвинутой типизации важно не перегружать типы. Рекомендуемый путь — хранить тип в аннотации, а ограничения — в метаданных.

    Для этого используется typing.Annotated и Field.

    Документация по Annotated: PEP 593 — Flexible function and variable annotations.

    Пример:

    Плюсы такого подхода:

  • читаемые типы (особенно если вы вводите алиасы вроде UserId)
  • единообразие ограничений
  • удобнее переиспользовать типы по всему проекту
  • Композиция моделей вместо наследования

    Наследование удобно, но в больших кодовых базах часто приводит к хрупким иерархиям: изменения в базовой модели влияют на десятки наследников.

    Композиция обычно устойчивее:

  • модель “собирается” из вложенных моделей
  • изменения локализованы
  • проще тестировать и переиспользовать
  • Пример композиции:

    Когда наследование оправдано:

  • вы действительно разделяете общий контракт (например, общие поля аудита)
  • вам нужно расширять модель “вверх”, не ломая базовую
  • Явная стратегия работы с лишними полями (extra)

    Параметр extra — архитектурная “вилка”, влияющая на безопасность и устойчивость интеграций.

  • extra='forbid': лучший выбор для домена и внутренних контрактов (лишнее — ошибка)
  • extra='ignore': часто полезен на входе, когда клиенты присылают больше полей, чем нужно
  • extra='allow': полезно редко (например, проксирование произвольных payload), требует дисциплины
  • Пример строгого запрета:

    Инварианты и валидаторы: минимум логики в валидаторах

    Валидаторы — мощный инструмент, но архитектурно опасный, если превращать их в “свалку бизнес-логики”. Рекомендации:

  • используйте валидаторы для нормализации и локальных инвариантов (формат, взаимосвязь пары полей)
  • бизнес-правила уровня “можно ли пользователю X купить товар Y” держите в сервисах домена
  • В Pydantic v2 используются @field_validator и @model_validator.

    Документация: Validators.

    Пример нормализации:

    Дискриминированные объединения: строгие полиморфные payload

    Когда у вас есть “одно из нескольких” сообщений/команд/событий, используйте discriminated union: поле-дискриминатор определяет конкретную модель.

    Документация: Discriminated Unions.

    Пример:

    Архитектурный эффект:

  • вы избегаете “мешков” вида dict[str, Any]
  • получаете проверяемый контракт для каждого варианта
  • упрощается обработка (тип известен после валидации)
  • TypeAdapter: единый валидатор для “не-моделей”

    Не всё в проекте должно быть BaseModel. Иногда нужно валидировать:

  • списки моделей
  • словари с типизированными значениями
  • union-типы
  • Для этого в v2 есть TypeAdapter.

    Документация: TypeAdapter.

    Пример:

    Архитектурно это помогает:

  • не плодить модели ради одной коллекции
  • централизовать входную валидацию на границе модуля
  • Конструирование без валидации: осознанный “быстрый путь”

    Иногда данные уже гарантированно корректны (например, пришли из вашего же хранилища, и вы контролируете схему). Тогда может быть полезно построить модель без валидации.

    В Pydantic v2 для этого используется model_construct.

    Документация: BaseModel.model_construct.

    Важно понимать архитектурный риск:

  • вы обходите инварианты и проверки
  • ошибка может “проехать дальше” и проявиться позже
  • Правило: используйте model_construct только в местах, где у вас действительно есть гарантия корректности данных и это зафиксировано в контракте слоя.

    Переиспользуемые типы: Value Object подход

    Вместо того чтобы везде писать str и int, вводите доменные типы:

  • UserId, OrderId
  • Email
  • CurrencyCode
  • Это улучшает читабельность и снижает шанс перепутать поля местами.

    Варианты реализации:

  • алиасы на основе Annotated (часто достаточно)
  • отдельные модели (если нужен набор правил и методов)
  • Пример алиаса:

    Примечание: регулярное выражение — это ограничение формата, но оно не доказывает, что почтовый ящик существует. Для архитектуры это означает, что “валидность формата” и “валидность как бизнес-факт” — разные уровни.

    Типизация и статический анализ: Pydantic не заменяет mypy/pyright

    Pydantic проверяет данные во время выполнения, а mypy/pyrightво время разработки.

    Ссылки:

  • mypy
  • Pyright
  • Практика для продвинутых проектов:

  • включайте строгий режим в type-checker там, где возможно
  • используйте Pydantic как runtime-гарантию на границах
  • проектируйте модели так, чтобы типы были “честными” (минимум Any, минимум неявных преобразований)
  • Резюме

  • Архитектура моделей начинается с разделения слоёв: transport, domain, persistence.
  • Строгость — управляемый параметр: мягче на входе, строже внутри.
  • Используйте ConfigDict для единых правил модели и строгие типы для точечной строгости.
  • Предпочитайте композицию моделей, а наследование используйте экономно.
  • Для полиморфных payload выбирайте discriminated unions.
  • TypeAdapter помогает валидировать сложные типы без создания лишних моделей.
  • model_construct — оптимизация с риском, применяйте только при гарантированной корректности данных.
  • В следующей логичной теме курса мы будем углубляться в валидацию, нормализацию и пользовательские ограничения так, чтобы валидаторы оставались предсказуемыми и архитектурно “тонкими”.

    2. Продвинутая валидация: validators, root/model validators

    Продвинутая валидация: validators, root/model validators

    В предыдущей статье мы рассматривали архитектуру моделей и строгую типизацию: разделение на transport/domain/persistence, осознанный выбор строгости и стратегию extra. Следующий шаг — настроить валидацию так, чтобы она оставалась предсказуемой, локальной и не превращалась в бизнес-логику.

    В Pydantic v2 валидация строится вокруг:

  • @field_validator для одного поля
  • @model_validator для инвариантов модели целиком (то, что в v1 часто решали через root_validator)
  • валидаторов в режимах before/after/wrap
  • Официальная документация: Validators.

    !Пайплайн выполнения валидаторов и места, где включаются before/after/wrap

    Принципы архитектурно безопасной валидации

    В продвинутых проектах качество моделей определяется не количеством валидаторов, а дисциплиной их применения.

    Рекомендации:

  • Валидаторы — для нормализации (trim, lowercasing), формата и локальных инвариантов.
  • Бизнес-правила уровня “разрешено ли пользователю сделать действие” — в сервисах домена, не в Pydantic.
  • Чем ближе к доменному слою, тем больше ценится строгость и явные ошибки.
  • Практически это означает:

  • В transport DTO допустима мягкая нормализация.
  • В domain — проверка инвариантов и запрет “скрытых” преобразований.
  • В persistence — строгий контроль сериализации/десериализации и совместимости схем.
  • Валидация одного поля: field_validator

    @field_validator запускается для конкретных полей и подходит для задач, которые не требуют знания остальных полей.

    Документация: Field validators.

    Нормализация строки

    Такую нормализацию обычно держат в transport (DTO), чтобы доменная модель получала уже “чистые” значения.

    mode='before' и mode='after'

    У валидатора поля есть режим:

  • mode='before': запускается до базовой валидации типа Pydantic
  • mode='after': запускается после базовой валидации типа
  • Профильное правило:

  • before полезен, когда вход может быть “грязным” (например, строка вместо числа) и вы хотите вручную привести данные к виду, пригодному для стандартной типовой валидации.
  • after полезен, когда значение уже типизировано, и вы проверяете/нормализуете “поверх” типа.
  • Пример: принять "42" на входе, но внутри модели хранить int.

    Если после этого limit всё равно не приводится к int, Pydantic вернёт понятную ошибку типизации.

    Валидатор для нескольких полей

    Один валидатор может быть назначен на несколько полей — полезно для одинаковой нормализации.

    Инварианты модели целиком: model_validator как замена root_validator

    В Pydantic v1 для проверки зависимости полей часто применяли @root_validator. В Pydantic v2 основной механизм — @model_validator.

    Документация: Model validators.

    mode='after': проверка инвариантов после построения модели

    Это наиболее “чистый” вариант для доменной логики уровня инвариантов: вы получаете уже типизированный объект и проверяете согласованность.

    Пример: у события должен быть указан хотя бы один канал доставки.

    Что важно:

  • В mode='after' вы возвращаете self.
  • Исключение ValueError превращается в валидационную ошибку Pydantic.
  • mode='before': нормализация входного словаря “до полей”

    Этот режим получает сырой вход (обычно dict) и полезен для случаев, когда:

  • нужно поддержать несколько форматов входа
  • нужно переименовать/слить поля
  • нужно “подготовить” структуру для дальнейшей типовой валидации
  • Пример: вход может приходить либо как full_name, либо как first_name/last_name.

    Архитектурная рекомендация: такие адаптации чаще держать в transport DTO, а доменную модель оставлять максимально однозначной.

    mode='wrap': обёртка вокруг всей валидации

    wrap — продвинутый режим, который позволяет:

  • измерять время валидации
  • добавлять контекстное логирование
  • единообразно перехватывать и переупаковывать ошибки
  • Концептуально: вы получаете функцию handler, которая выполняет стандартную валидацию, а вы решаете, когда и как её вызвать.

    wrap имеет смысл использовать точечно — это инструмент инфраструктуры, а не доменных правил.

    Порядок выполнения и типичные ловушки

    Важно понимать, когда выполняется валидатор, иначе можно получить неожиданные эффекты.

    Практические правила:

  • Если вам нужно привести вход к ожидаемому типу, используйте mode='before'.
  • Если вы хотите проверять значения уже гарантированного типа, используйте mode='after'.
  • Для взаимосвязей полей используйте model_validator, а не “хак” с чтением чужих полей в field_validator.
  • Типичная ошибка: попытаться в валидаторе поля прочитать другое поле, которое ещё не провалидировано. В v2 это может быть особенно неприятно из-за порядка обработки.

    Явные, стабильные ошибки: PydanticCustomError

    Для API и внутренних контрактов полезно, чтобы ошибки были:

  • стабильными по коду
  • предсказуемыми по сообщению
  • пригодными для машинной обработки
  • Вместо “случайных” ValueError("...") можно использовать PydanticCustomError.

    Документация по пользовательским ошибкам находится в разделе ошибок: Errors.

    Пример:

    Архитектурная польза: фронтенд/клиенты могут реагировать на weak_password как на код, не парся текст.

    Валидаторы и строгая типизация: как не “сломать” контракт

    Из прошлой темы ключевая идея: строгость — управляемая.

    Как валидаторы могут ухудшить строгость:

  • Если в доменной модели вы активно используете mode='before' и “чините” вход, то внутрь начинают проникать неявные форматы.
  • Если валидаторы молча подставляют дефолты вместо ошибки, контракт становится размытым.
  • Рекомендуемый баланс:

  • В DTO допустима нормализация и “поддержка форматов”.
  • В домене валидаторы должны скорее отклонять, чем “угадывать”.
  • Мини-шаблоны по слоям

    Transport DTO

    Часто:

  • model_validator(mode='before') для адаптации форматов
  • field_validator(mode='before') для trimming и приведения простых типов
  • extra='ignore' или осознанный выбор поведения лишних полей
  • Domain

    Часто:

  • model_validator(mode='after') для инвариантов
  • строгие типы и/или ConfigDict(strict=True)
  • extra='forbid'
  • Persistence

    Часто:

  • минимальная нормализация
  • явные правила сериализации
  • контроль совместимости схем
  • Резюме

  • field_validator — инструмент для нормализации и проверок одного поля; выбирайте before для подготовки входа и after для проверок на типизированных значениях.
  • model_validator — основной механизм для инвариантов модели и замена root_validator из v1.
  • wrap подходит для инфраструктурных задач вокруг валидации.
  • Для стабильных ошибок полезно применять PydanticCustomError.
  • Архитектурно удерживайте “умную” нормализацию на уровне transport, а домен делайте строгим и однозначным.
  • 3. Настройка сериализации: JSON, алиасы, кастомные энкодеры

    Настройка сериализации: JSON, алиасы, кастомные энкодеры

    После архитектуры моделей и строгой типизации, а затем продвинутой валидации, следующий критичный слой качества в Pydantic — сериализация. Именно на выходе (в JSON, события, логи, кэш) чаще всего проявляются ошибки контрактов: неправильные имена полей, “лишние” данные, нестабильные форматы дат и чисел, утечки внутренних полей.

    В Pydantic v2 сериализация строится вокруг:

  • model_dump() и model_dump_json() для моделей
  • параметров исключения полей (exclude_none, exclude_unset, exclude_defaults)
  • алиасов для ввода и вывода (alias, serialization_alias, validation_alias)
  • кастомной сериализации через @field_serializer и @model_serializer
  • сериализации “не-моделей” через TypeAdapter
  • Документация:

  • Pydantic Serialization
  • Pydantic Alias
  • !Пайплайн сериализации и точки, где настраиваются правила

    Сериализация как часть архитектуры слоёв

    Связь с предыдущими темами курса:

  • Transport DTO обычно ориентированы на внешний контракт: алиасы, “красивые” имена, возможно, обратная совместимость.
  • Domain чаще хранит “честные” внутренние типы и строгие имена, а сериализация становится явным шагом “на границе”.
  • Persistence требует стабильных форматов для хранения, версионирования и миграций.
  • Практический вывод: сериализацию стоит проектировать так же осознанно, как и валидацию. Особенно если одна и та же модель уходит и в API, и в очередь, и в логирование.

    Базовые операции: model_dump() и model_dump_json()

    В Pydantic v2 основная пара методов:

  • model_dump() возвращает Python-структуру (обычно dict), пригодную для дальнейшей обработки.
  • model_dump_json() возвращает JSON-строку.
  • Пример:

    Рекомендация по архитектуре:

  • если дальше вы используете свой JSON-энкодер, логгер или брокер сообщений, чаще удобнее работать с результатом model_dump()
  • если вам нужен “готовый JSON” для HTTP-ответа или записи, используйте model_dump_json()
  • Контроль состава ответа: exclude_none, exclude_unset, exclude_defaults

    Три флага решают три разных задачи и их важно не путать.

  • exclude_none=True убирает поля со значением None
  • exclude_unset=True убирает поля, которые не были переданы при создании модели (даже если у них есть значение по умолчанию)
  • exclude_defaults=True убирает поля, которые равны значению по умолчанию
  • Пример:

    Архитектурные правила:

  • для PATCH/частичных обновлений чаще всего нужен exclude_unset=True (важно различать “не прислали” и “прислали null”)
  • для компактных публичных ответов иногда применяют exclude_none=True, но осторожно: null может быть частью контракта
  • exclude_defaults=True полезен для экономии трафика, но может ломать клиентов, которые ожидают явные поля
  • Алиасы: как управлять именами полей на входе и на выходе

    Алиасы решают две частые проблемы:

  • внешний API использует имена, которые не совпадают с вашими Python-именами
  • нужно поддерживать несколько вариантов входных ключей (обратная совместимость)
  • В Pydantic v2 важно различать:

  • alias как “общее” альтернативное имя
  • validation_alias как имя(имена) для ввода
  • serialization_alias как имя для вывода
  • Пример, когда вход и выход должны отличаться:

    Здесь:

  • вход принимает id
  • внутри домена поле называется user_id
  • наружу поле уходит как userId
  • Это хороший компромисс между “честными” внутренними именами и требованиями внешнего контракта.

    Выбор стратегии by_alias

  • by_alias=False (по умолчанию) удобен для внутреннего обмена между модулями
  • by_alias=True стоит включать на границе наружу: HTTP-ответы, события для внешних потребителей, публичные JSON-контракты
  • Практика: держать “публичную” сериализацию в одном месте (например, слой представления/контроллера), чтобы не “заразить” домен форматами API.

    Кастомная сериализация: @field_serializer

    Иногда тип корректный, но формат должен быть особым:

  • деньги: хранить как Decimal, отдавать строкой
  • даты: хранить как datetime, отдавать в конкретном формате
  • внутренние идентификаторы: хранить как int, отдавать как строку
  • Для этого в Pydantic v2 применяют @field_serializer.

    Пример: отдавать Decimal как строку, чтобы избежать потери точности в JSON.

    Архитектурная мысль: сериализатор должен быть тонким и детерминированным. Он не должен “подтягивать данные из базы” или выполнять бизнес-решения. Его задача — формат.

    Кастомная сериализация модели целиком: @model_serializer

    @model_serializer удобен, когда:

  • нужно поменять форму выходного объекта (например, сделать “плоский” JSON)
  • нужно добавить вычисляемые поля
  • нужно совместить несколько внутренних полей в одно внешнее
  • Пример: внутри модель хранит first_name и last_name, а наружу отдаём fullName.

    Важно:

  • такой подход меняет весь контракт сериализации модели
  • часто его применяют для отдельной “view-модели” (представления), чтобы не ломать внутреннее использование
  • “Кастомные энкодеры” и json_encoders: что использовать в v2

    В Pydantic v1 популярной настройкой были “глобальные” json_encoders в конфигурации. В v2 основной рекомендуемый путь — @field_serializer и @model_serializer, потому что они:

  • привязаны к модели или полю
  • лучше читаются в коде
  • проще тестируются и эволюционируют
  • Если вам нужна точечная, явная настройка сериализации — выбирайте сериализаторы.

    Ссылаться на общую концепцию сериализации лучше через официальный раздел: Pydantic Serialization.

    Сериализация не-моделей: TypeAdapter.dump_python() и TypeAdapter.dump_json()

    Как обсуждали в теме про архитектуру, не всё обязано быть BaseModel. Если у вас есть тип вроде list[UserPublic] или dict[str, Price], удобно использовать TypeAdapter.

    Пример:

    Архитектурная польза:

  • единая точка сериализации для коллекций и сложных типов
  • не нужно создавать “контейнерную модель” только ради одного списка
  • Стабильность контрактов: рекомендации для продвинутых проектов

  • Включайте extra='forbid' для внутренних контрактов, а на публичной границе аккуратно выбирайте, что именно отдаёте.
  • Разделяйте “модель домена” и “модель представления”, если внешний формат сильно отличается.
  • Используйте serialization_alias для стабильного JSON-API, не меняя Python-имена.
  • Явно выбирайте стратегию exclude_unset/exclude_none/exclude_defaults под конкретный протокол.
  • Тестируйте сериализацию как контракт: снапшот-тесты на model_dump(by_alias=True) часто окупаются.
  • Резюме

  • model_dump() и model_dump_json() — базовые инструменты сериализации модели в Python-структуру и JSON.
  • exclude_unset, exclude_none, exclude_defaults решают разные задачи и по-разному влияют на контракт.
  • Алиасы стоит разделять на ввод и вывод: validation_alias и serialization_alias.
  • Для кастомного формата используйте @field_serializer и @model_serializer.
  • Для сериализации коллекций и сложных типов без моделей применяйте TypeAdapter.
  • Далее по курсу логично рассматривать более глубокие сценарии: вычисляемые поля, управление схемой (JSON Schema) и версионирование контрактов, чтобы сериализация оставалась совместимой при развитии системы.

    4. Generics, discriminated unions и сложные типы

    Generics, discriminated unions и сложные типы

    В прошлых статьях курса мы выстроили базовую архитектуру моделей (transport/domain/persistence), настроили строгую типизацию и научились управлять валидацией и сериализацией. Следующий уровень практики в Pydantic v2 начинается там, где данные становятся полиморфными и композиционными:

  • один и тот же контейнер данных должен работать с разными типами (generics)
  • payload может быть “одним из нескольких вариантов” (discriminated unions)
  • структуры становятся вложенными, рекурсивными, параметризованными и “не помещаются” в один BaseModel без потери строгости (сложные типы)
  • Эта тема важна архитектурно: она помогает удерживать контракты строгими, но при этом не плодить десятки почти одинаковых моделей.

    Документация Pydantic v2, на которую будем опираться:

  • Generic models
  • Unions
  • Discriminated unions
  • TypeAdapter
  • !Как generics и discriminated unions обычно встраиваются в слои моделей

    Generics в Pydantic: типобезопасные контейнеры

    Generics (обобщённые типы) позволяют описать модель-контейнер, которая остаётся строгой, но может быть переиспользована с разными “внутренними” типами.

    Архитектурные случаи, где generics особенно полезны:

  • типизированные ответы API: “обёртка” + payload
  • пагинация: Page[T] для любых сущностей
  • единый формат событий: метаданные + payload
  • результаты операций: Ok[T]/Error (часто в связке с union)
  • Пример: пагинация Page[T]

    Теперь можно получить строгие варианты без копирования модели:

    Что это даёт:

  • items валидируется как list[UserPublic], а не как “список словарей”
  • IDE и статический анализ видят Page[UserPublic] как конкретный тип
  • формат контейнера (page, page_size, total) единообразен во всём проекте
  • Где generics держать в архитектуре слоёв

  • Transport DTO
  • 1. Generics можно использовать, но осторожно: внешние контракты часто меняются, и иногда выгоднее иметь явную DTO-модель под конкретный эндпоинт.
  • Domain
  • 1. Generics отлично подходят: контейнеры домена (страницы, результаты, пачки команд) обычно стабильны.
  • Persistence
  • 1. Generics применимы, если слой хранения работает с типизированными структурами (например, кеш “ключ -> объект T”).

    Важная дисциплина: generic-модель не должна “размывать” строгую типизацию

    Типичная ошибка: вместо Page[UserPublic] использовать “универсальную” страницу с items: list[dict].

  • так быстрее начать
  • но это ломает контракт: ошибки переедут из слоя валидации в бизнес-код
  • Правило: если вы уже знаете тип элементов, параметризуйте generic.

    Discriminated unions: строгий полиморфизм вместо dict[str, object]

    Discriminated union решает задачу “объект одного из нескольких типов” так, чтобы:

  • Pydantic мог однозначно выбрать модель по полю-дискриминатору
  • вы получили типизированный результат без ручных if payload['kind'] == ...
  • сериализация была стабильной и предсказуемой
  • Базовый паттерн: kind + Literal

    Ключевые условия корректной дискриминации:

  • в каждом варианте union должно быть поле kind
  • kind должен быть Literal[...] с уникальными значениями
  • union должен быть “помечен” дискриминатором через Field(discriminator='kind')
  • Что даёт extra='forbid' вместе с union

    Если вы проектируете внутренний контракт (domain/internal events), то extra='forbid' полезен для каждого варианта:

  • в CardPayment не “проскочит” поле, относящееся к CashPayment
  • ошибки становятся ближе к источнику данных
  • Это напрямую продолжает идеи из статьи про архитектуру и строгую типизацию.

    Discriminated union внутри generic-контейнера

    Частый практический сценарий: единый конверт события, а payload полиморфный.

    Теперь можно собрать строгий тип:

    Архитектурный эффект:

  • транспортный слой может валидировать “конверт” одинаково для всех событий
  • домен обрабатывает payload как конкретный тип (через union-дискриминацию)
  • Когда discriminated union предпочтительнее обычного Union

    Обычный Union[A, B] без дискриминатора заставляет Pydantic угадывать подходящий вариант. Это может приводить к:

  • неожиданному выбору варианта
  • труднообъяснимым ошибкам, если модели похожи по структуре
  • Discriminated union делает выбор явным, а контракт легче документировать.

    Сложные типы: композиция, рекурсия, “не-модели” и TypeAdapter

    Под сложными типами здесь будем понимать не “сложно валидировать”, а “сложно выразить одной плоской моделью”:

  • вложенные структуры (модель в модели)
  • коллекции моделей и union-типов
  • рекурсивные структуры
  • структуры, где модель избыточна, но тип сложный
  • Вложенные структуры как основной способ масштабирования схем

    Композиция (вложенные модели) обычно лучше наследования:

    Это продолжает линию из первой статьи: композиция локализует изменения и делает контракт устойчивее.

    Рекурсивные структуры: дерево, граф, вложенные узлы

    В реальных доменах встречаются:

  • дерево категорий
  • комментарии с ответами
  • организационные структуры
  • Рекурсивную модель можно описать так:

    Практическая рекомендация:

  • следите за дефолтами коллекций
  • если вам нужно гарантировать отсутствие разделяемого состояния, используйте default_factory
  • “Не-модели” и TypeAdapter: валидировать тип, а не класс

    Иногда у вас нет “сущности”, чтобы заводить BaseModel, но есть сложный тип:

  • list[Payment]
  • dict[str, Page[UserPublic]]
  • list[tuple[int, str]]
  • Для этого в Pydantic v2 есть TypeAdapter.

    Архитектурный смысл:

  • вы не плодите “контейнерные модели” ради одного списка
  • при этом получаете ту же строгость, что и у BaseModel
  • Контроль поведения union без дискриминатора

    Иногда дискриминатор невозможен (например, вы интегрируетесь с чужим форматом). Тогда вы всё равно можете использовать Union, но важно осознанно управлять предсказуемостью.

    В Pydantic есть настройки режима выбора варианта union (подробнее в Unions). Практическая идея такая:

  • “умный” выбор удобен, но может давать неожиданные результаты на похожих схемах
  • “лево-направо” делает поведение проще для объяснения, но требует аккуратного порядка вариантов
  • Если формат ваш, чаще выгоднее добавить дискриминатор и превратить задачу в discriminated union.

    Связь с валидацией и сериализацией

    Эта тема напрямую продолжает две предыдущие статьи.

    Generics и валидаторы

  • @field_validator и @model_validator работают и в generic-моделях
  • держите нормализацию в transport, а инварианты в domain
  • не превращайте generic-контейнер в “бизнес-объект”: его задача обычно инфраструктурная
  • Unions и сериализация

  • model_dump(by_alias=True) важен для публичных контрактов, в том числе для union-вариантов
  • если у вас разные форматы на выходе, лучше делать отдельные view-модели, чем “изгибать” доменную модель @model_serializer
  • Практические правила проектирования

  • Используйте generics для стабильных контейнеров: страница, конверт, результат.
  • Для полиморфных payload предпочитайте discriminated union с Literal-дискриминатором.
  • Для внутренних контрактов включайте extra='forbid' на вариантах union.
  • Для сложных коллекций и “типов без сущности” используйте TypeAdapter.
  • Если дискриминатор добавить нельзя, внимательно отнеситесь к режиму и порядку вариантов Union.
  • Резюме

  • Generics позволяют выразить типобезопасные контейнеры и переиспользовать их без копирования моделей.
  • Discriminated unions дают строгий полиморфизм и убирают угадывание варианта union.
  • Сложные типы масштабируются через композицию, рекурсию и TypeAdapter для “не-моделей”.
  • Архитектурно это помогает удерживать строгие контракты на границах системы и не растворять типы в dict[str, object].
  • 5. JSON Schema и OpenAPI: генерация и кастомизация

    JSON Schema и OpenAPI: генерация и кастомизация

    В предыдущих статьях курса мы построили архитектуру моделей (transport/domain/persistence), научились управлять строгой типизацией, валидацией и сериализацией, а также разобрали generics и discriminated unions. Логичное продолжение — документирование контрактов.

    В продвинутых системах важно, чтобы контракт был:

  • проверяемым (runtime-валидация через Pydantic)
  • сериализуемым (стабильные форматы на выходе)
  • документируемым (JSON Schema и OpenAPI)
  • JSON Schema и OpenAPI позволяют:

  • генерировать формальное описание входных и выходных данных
  • публиковать спецификацию API для фронтенда, интеграторов и тестов
  • автоматизировать SDK, мок-серверы, контрактные тесты и валидацию на границах
  • Документация:

  • JSON Schema
  • OpenAPI Specification
  • Pydantic JSON Schema
  • !Общая картина: как Pydantic порождает JSON Schema и как она попадает в OpenAPI

    Что такое JSON Schema и зачем оно Pydantic

    JSON Schema — это формат описания структуры JSON-документа: какие поля есть, какие типы, какие ограничения (минимум/максимум, формат, enum), какие поля обязательны.

    Pydantic умеет генерировать JSON Schema из Python-типов и ограничений, которые уже есть в ваших моделях:

  • аннотации типов (int, str, list[T], Union, Literal)
  • ограничения через Field(...) и Annotated[...]
  • правила extra и required/optional
  • структурные особенности вроде discriminated unions
  • Архитектурный вывод, связанный с темой слоёв моделей:

  • DTO-модели часто становятся источником публичной схемы (вход/выход API)
  • доменные модели полезны как внутренние контракты, но их схему не всегда стоит публиковать
  • view-модели (модели представления) часто являются лучшим местом для “красивого” и стабильного OpenAPI
  • Генерация JSON Schema в Pydantic v2

    Схема для BaseModel: model_json_schema()

    У любой модели есть метод model_json_schema(), который возвращает Python-словарь (его можно сериализовать в JSON).

    Практика:

  • храните получившуюся схему как артефакт контрактов (например, в репозитории или в CI)
  • используйте снапшот-тесты, чтобы замечать изменения схемы при рефакторинге
  • Схема для “не-моделей”: TypeAdapter.json_schema()

    Как и в прошлых темах, не всё обязано быть BaseModel. Если вы хотите описать, например, list[UserPublic] или dict[str, int], используйте TypeAdapter.

    Это особенно полезно, если ваш контракт — не объект, а коллекция (например, “эндпоинт возвращает список”).

    Валидационная и сериализационная схема: почему это важно

    В прошлой статье про сериализацию мы разделяли:

  • что модель принимает (validation)
  • что модель отдаёт (serialization)
  • Эта же идея применима к JSON Schema.

    mode='validation' и mode='serialization'

    В Pydantic v2 JSON Schema можно строить в разных режимах:

  • mode='validation': схема описывает вход (учитывает правила валидации)
  • mode='serialization': схема описывает выход (учитывает правила сериализации)
  • Это особенно важно, если вы используете разные алиасы для ввода и вывода (validation_alias и serialization_alias) из темы про сериализацию.

    Пример:

    Архитектурное правило:

  • для документации входных данных (request body) ориентируйтесь на validation
  • для документации ответов (response body) ориентируйтесь на serialization
  • Как Pydantic попадает в OpenAPI

    OpenAPI — это спецификация, описывающая HTTP API: пути, методы, параметры, request/response тела, коды ответов и схемы данных.

    В экосистеме Python самый распространённый путь — автоматическая генерация OpenAPI из Pydantic-моделей во фреймворке FastAPI.

    Документация:

  • FastAPI
  • FastAPI OpenAPI
  • FastAPI Schema Customization
  • Минимальный пример FastAPI: request/response схемы

    Что происходит:

  • UserCreateDTO становится схемой request body
  • UserPublic становится схемой response body
  • обе схемы попадают в раздел components/schemas OpenAPI и переиспользуются через $ref
  • Важно: OpenAPI — это не “просто документация”, а формат контракта, который часто становится источником истины для других команд.

    Кастомизация JSON Schema на уровне полей

    Самый устойчивый способ кастомизации — добавлять метаданные в Field (или Annotated[..., Field(...)]).

    title, description, examples

    Практика:

  • description должен объяснять смысл поля, а не повторять его имя
  • examples полезнее, чем абстрактные описания, особенно для строковых форматов
  • json_schema_extra для точечных расширений

    Если вам нужно добавить JSON Schema-атрибуты, которые не имеют прямого аналога в аргументах Field, используйте json_schema_extra.

    Здесь важно помнить архитектурный принцип: схема — это контракт. Если вы добавляете enum в схему, убедитесь, что валидация действительно запрещает другие значения (например, через Literal или через валидатор).

    Кастомизация JSON Schema на уровне модели

    model_config = ConfigDict(...) и json_schema_extra

    На уровне модели часто нужно:

  • описать модель целиком
  • добавить примеры объекта
  • пометить поле как устаревшее (если ваш инструментарий это поддерживает)
  • В Pydantic v2 можно расширять схему через json_schema_extra в конфигурации.

    Рекомендация: применяйте такие расширения для публичных DTO/view-моделей, а доменные модели держите более нейтральными.

    Discriminated unions и схема: oneOf + discriminator

    Из прошлой темы про discriminated unions важное следствие: правильно спроектированный union автоматически превращается в хорошо документируемую схему.

    В JSON Schema/OpenAPI это обычно выражается как:

  • oneOf: “объект соответствует одному из вариантов”
  • discriminator: “выбор варианта определяется полем kind
  • Практическая польза:

  • документация явно показывает все варианты payload
  • клиенты могут генерировать типобезопасные структуры
  • вы избегаете неявного Union-угадывания и улучшаете предсказуемость контракта
  • Generics и схема: переиспользуемые контейнеры без потери контракта

    Generic-модели вроде Page[T] часто становятся “контейнерным стандартом” для API. При этом схема должна оставаться строгой: items должны быть определённого типа.

    Архитектурное правило: если ваш API реально возвращает “страницу пользователей”, документируйте именно Page[UserPublic], а не абстрактный Page[dict].

    Пользовательские типы и кастомная схема

    Иногда доменная типизация требует отдельного типа (например, CurrencyCode, OrderId, ULID), но вы хотите:

  • сохранить строгую runtime-валидацию
  • сделать схему читабельной и полезной
  • В Pydantic v2 для глубокого контроля над схемой пользовательских типов используется специальный хук типа __get_pydantic_json_schema__.

    Ниже пример идеи: мы создаём отдельный тип кода валюты, который выглядит как строка, но в схеме имеет описание и пример.

    Практика:

  • такую кастомизацию стоит делать для типов, которые массово используются в публичных DTO
  • если у типа есть строгие правила формата, документируйте их в описании и (по возможности) подкрепляйте валидацией (например, через pattern или валидатор)
  • Стратегия стабильности: как не “ломать” схемы при эволюции API

    Схемы меняются по тем же причинам, что и код: требования, рефакторинг, новые поля. Но изменение схемы часто означает изменение контракта.

    Рекомендации:

  • разделяйте модели: домен не обязан совпадать с публичной схемой; для внешних контрактов используйте DTO/view-модели
  • используйте validation_alias, чтобы принимать старые имена входных полей (обратная совместимость), но аккуратно документируйте “основное” имя
  • покрывайте model_json_schema() снапшот-тестами, как контрактный артефакт
  • держите extra='forbid' на внутренних контрактах, чтобы схема соответствовала реальным ожиданиям и не допускала “случайных” полей
  • Резюме

  • JSON Schema — формальное описание структуры данных; Pydantic v2 генерирует его из типов, Field-ограничений, валидаторов и union/generic-структур.
  • Для моделей используйте model_json_schema(), для сложных типов без BaseModelTypeAdapter.json_schema().
  • Генерируйте схему в нужном режиме: validation для входа и serialization для выхода, особенно если используются разные алиасы.
  • OpenAPI чаще всего строится поверх JSON Schema (например, в FastAPI) и превращает ваши модели в публичный контракт.
  • Кастомизация схемы делается через Field(...), json_schema_extra, конфигурацию модели и (в продвинутых случаях) через хуки пользовательских типов.
  • 6. Интеграции: FastAPI, ORM, настройки приложений и конфиги

    Интеграции: FastAPI, ORM, настройки приложений и конфиги

    В предыдущих темах курса мы последовательно построили контрактный слой вокруг данных:

  • архитектура моделей и строгая типизация
  • продвинутая валидация (field_validator, model_validator)
  • управляемая сериализация (алиасы, сериализаторы)
  • generics и discriminated unions
  • JSON Schema и OpenAPI
  • Эта статья переводит всё это в практическую плоскость: как использовать Pydantic в реальном приложении, где есть HTTP API (FastAPI), база данных (ORM), а также настройки (конфиги, переменные окружения, секреты).

    !Общая карта, где Pydantic участвует в запросах/ответах, маппинге ORM и настройках

    FastAPI и Pydantic: границы ввода и вывода

    FastAPI строит большинство своих контрактов вокруг Pydantic-моделей:

  • тело запроса (request body) валидируется как Pydantic-модель
  • тело ответа может быть приведено к response_model
  • OpenAPI генерируется на базе схем моделей
  • Ссылки:

  • FastAPI
  • FastAPI Response Model
  • Pydantic JSON Schema
  • Разделяйте модели для запроса, ответа и домена

    Идея из первой статьи (transport/domain/persistence) особенно важна в FastAPI:

  • модель запроса должна быть удобна для клиента и устойчивой к эволюции API
  • модель ответа должна быть стабильным публичным контрактом
  • доменная модель не обязана совпадать с API
  • Пример минимального набора моделей:

    Здесь:

  • UserCreateDTO принимает вход как транспортный контракт
  • User описывает внутреннюю сущность (строго)
  • UserPublic управляет именами полей для ответа через serialization_alias
  • Подробнее про алиасы:

  • Pydantic Alias
  • Контроль состава ответа: exclude_unset как стандарт для PATCH

    В FastAPI часто есть частичные обновления. Здесь важно различать:

  • поле не прислали (не надо трогать в БД)
  • поле прислали как null (надо явно очистить)
  • Ровно для этого существует exclude_unset=True при сериализации:

    Практическое правило:

  • для PATCH в репозиторий/ORM обычно используйте exclude_unset=True
  • Сериализация и флаги исключения:

  • Pydantic Serialization
  • Валидационные ошибки как часть API контракта

    Если вы в валидаторах используете PydanticCustomError, то ошибки имеют стабильный код. Это удобно для клиентов (например, фронтенда), которые могут показывать разные сообщения или подсветку по коду.

    В FastAPI это превратится в стандартный ответ 422, но с предсказуемым содержимым ошибки.

    Валидаторы:

  • Pydantic Validators
  • Generics и OpenAPI в FastAPI

    Контейнеры вроде Page[T] (из прошлой статьи про generics) хорошо ложатся на FastAPI: вы можете использовать конкретизацию Page[UserPublic] как response_model, и это попадёт в OpenAPI.

    ORM и Pydantic: маппинг сущностей без утечек слоёв

    ORM обычно представляет данные как Python-объекты, а API хочет получить JSON. Pydantic может выступать как слой безопасного преобразования между ними.

    На практике важно:

  • не “вытащить наружу” лишние поля
  • не привязать API-контракт к структуре ORM
  • не спровоцировать лишние SQL-запросы при сериализации отношений
  • Валидация из ORM-объекта: from_attributes

    В Pydantic v2 подход “ORM mode” заменён на чтение атрибутов через from_attributes.

    Документация:

  • Pydantic Models
  • Вариант 1: включить на модели.

    Вариант 2: включить при валидации.

    Когда какой вариант выбирать:

  • на уровне модели удобно, если эта схема почти всегда строится из ORM
  • на уровне вызова удобно, если модель используется и для словарей, и для объектов
  • Отношения ORM и проблема N+1 при сериализации

    Если ваша Pydantic-схема включает связанные сущности (например, user.posts: list[PostPublic]), то при обращении к атрибутам ORM могут запускаться дополнительные запросы.

    Практика:

  • перед тем как строить Pydantic-модель из ORM, заранее загрузите нужные отношения
  • Для SQLAlchemy это обычно означает использование eager loading (selectinload, joinedload) на уровне запроса.

    Документация:

  • SQLAlchemy
  • Не превращайте Pydantic-сериализацию в слой доступа к данным

    Архитектурное правило из тем про валидацию и сериализацию:

  • сериализаторы (field_serializer, model_serializer) должны быть детерминированными и “тонкими”
  • Плохой сценарий:

  • сериализатор лезет в БД, дергает внешние сервисы или читает ленивые отношения ORM
  • Хороший сценарий:

  • репозиторий/сервис домена собирает все данные
  • Pydantic только валидирует и форматирует
  • Два устойчивых паттерна интеграции с ORM

  • DTO/view-модель поверх ORM:
  • - ORM-объект читается через from_attributes - наружу отдаётся только публичная схема
  • Явный маппинг ORM → домен → view:
  • - ORM маппится в доменную модель (строго, без “ORM артефактов”) - доменная модель маппится в view-модель

    Первый путь быстрее, второй лучше держит границы в больших системах.

    Если вы используете SQLModel, он объединяет идеи Pydantic и SQLAlchemy в одну модель, но архитектурные принципы слоёв всё равно остаются важными.

  • SQLModel
  • Настройки приложений: pydantic-settings, окружение, файлы конфигов

    В Pydantic v2 настройки приложения вынесены в отдельный пакет pydantic-settings.

    Документация:

  • Pydantic Settings
  • Базовый пример BaseSettings

    Что здесь происходит:

  • настройки читаются из переменных окружения
  • env_prefix='APP_' означает, что debug ищется как APP_DEBUG
  • Field(alias='DATABASE_URL') позволяет поддержать стандартное имя переменной
  • extra='ignore' делает настройки устойчивыми к “лишним” переменным окружения
  • Вложенные настройки и env_nested_delimiter

    Для крупных приложений удобно группировать настройки:

    Тогда переменная окружения APP_DB__HOST попадёт в settings.db.host.

    Подключение файлов конфигурации как источника

    Сильная сторона pydantic-settings в том, что вы можете определить порядок и набор источников: окружение, .env, secrets, JSON-файл, свой источник.

    Типовой сценарий:

  • в проде главный источник это env и secrets
  • локально удобно иметь config.json или .env
  • Пример добавления JSON-файла как источника через settings_customise_sources:

    Ключевая идея:

  • вы централизуете “как собирается конфиг”
  • вы сохраняете строгую валидацию настроек
  • вы можете тестировать конфиг как обычную Pydantic-модель
  • Настройки как dependency в FastAPI

    Часто настройки нужно внедрять в зависимости FastAPI.

    Зачем кешировать:

  • чтение и валидация настроек могут быть дорогими
  • настройки обычно неизменяемы в процессе работы приложения
  • Документация по зависимостям:

  • FastAPI Dependencies
  • Практический чек-лист интеграций

  • Выделяйте отдельные модели для запроса, ответа и домена, не смешивайте их.
  • Для PATCH используйте exclude_unset=True, чтобы корректно отличать “не прислали” от “прислали null”.
  • Для чтения из ORM используйте from_attributes (в конфиге модели или в model_validate).
  • Не допускайте, чтобы сериализация вызывала ленивые запросы ORM: загружайте отношения заранее.
  • Используйте pydantic-settings для строгих, тестируемых настроек; при необходимости подключайте JSON-файлы и другие источники через settings_customise_sources.
  • Резюме

  • В FastAPI Pydantic-модели это контракт: request/response/OpenAPI, поэтому важно держать разделение transport/domain/view.
  • Для частичных обновлений ключевой инструмент это exclude_unset=True.
  • Интеграция с ORM в Pydantic v2 строится вокруг from_attributes; это удобный и контролируемый мост между объектами и схемами.
  • Настройки приложения следует реализовывать через pydantic-settings, подключая источники (env, dotenv, secrets, файлы) в явном и тестируемом порядке.
  • 7. Производительность, тестирование и миграция Pydantic v1 → v2

    Производительность, тестирование и миграция Pydantic v1 → v2

    В предыдущих темах курса мы построили архитектуру моделей (transport/domain/persistence), настроили строгую типизацию, валидацию, сериализацию, разобрали generics, discriminated unions и генерацию JSON Schema/OpenAPI, а затем посмотрели интеграции с FastAPI/ORM и конфигами.

    Эта статья отвечает на три практических вопроса, которые почти всегда возникают в зрелых проектах:

  • Как сделать Pydantic быстрее там, где он реально влияет на latency и CPU?
  • Как тестировать модели как контракт, а не как случайный набор полей?
  • Как безопасно мигрировать кодовую базу с Pydantic v1 на v2, не сломав интеграции и схемы?
  • Мы будем опираться на Pydantic v2 и официальные материалы:

  • Pydantic Migration Guide
  • Pydantic Validators
  • Pydantic Serialization
  • Pydantic TypeAdapter
  • Pydantic JSON Schema
  • !Карта темы: где находятся точки оптимизации, какие тесты страхуют контракт и как раскладывается миграция

    Производительность в Pydantic v2

    Pydantic v2 построен на pydantic-core, поэтому в среднем быстрее v1. Но в реальных сервисах “быстрее” зависит от архитектуры использования: сколько раз вы валидируете, где сериализуете, как часто пересоздаёте адаптеры типов, и сколько логики добавили в валидаторы.

    Откуда обычно берётся стоимость

    Наиболее частые источники затрат:

  • Валидация больших объёмов входных данных (списки, события очередей, массовые импорты).
  • Валидация в горячих участках кода “по мелочи”, но много раз (например, на каждом сообщении/запросе).
  • Тяжёлая сериализация (частые model_dump(by_alias=True) и model_dump_json() на больших моделях).
  • Валидаторы, которые делают больше, чем нормализацию и локальные проверки.
  • Архитектурное правило из предыдущих статей курса сохраняется: держите “умную” нормализацию в transport, строгие инварианты в domain, а сериализацию делайте явным шагом на границе. Это не только про дизайн, но и про производительность.

    Переиспользуйте TypeAdapter для не-моделей и коллекций

    Если ваш контракт — это не один BaseModel, а тип вроде list[Event] или dict[str, Price], используйте TypeAdapter и не создавайте его на каждом вызове.

    Практический эффект:

  • TypeAdapter компилирует логику валидации для типа; переиспользование уменьшает накладные расходы.
  • Вы сохраняете строгость (включая discriminated union), но не плодите контейнерные модели.
  • Документация: Pydantic TypeAdapter

    Выбирайте правильный входной метод: Python vs JSON

    В v2 есть разные пути валидации:

  • Model.model_validate(obj) для Python-структур (dict, list).
  • Model.model_validate_json(json_bytes_or_str) для JSON.
  • Если у вас уже есть JSON-строка/байты, чаще выгоднее валидировать напрямую из JSON, чем сначала делать json.loads, а затем model_validate.

    Избегайте скрытой “дорогой” работы в валидаторах и сериализаторах

    Из прошлых статей:

  • валидаторы и сериализаторы должны быть тонкими и детерминированными
  • Производительный и архитектурно корректный подход:

  • валидатор проверяет формат/инвариант и возвращает значение
  • Непроизводительный и архитектурно рискованный подход:

  • валидатор ходит в БД, читает файл, обращается к сети
  • сериализатор триггерит ленивые отношения ORM (N+1)
  • Если данных не хватает для корректной валидации, это признак того, что правило должно жить в сервисе домена, а не в Pydantic.

    Осознанно используйте model_construct для доверенных данных

    model_construct создаёт модель без валидации. Это может быть очень быстро, но снимает все гарантии.

    Практическое правило:

  • используйте model_construct только на слоях, где контракт уже гарантирован (например, после чтения из “своего” стабильного хранилища)
  • фиксируйте это решением в код-ревью и тестами, иначе ошибки “уедут” в бизнес-логику
  • Документация: BaseModel.model_construct

    Сериализация: меньше работы, меньше аллокаций

    Сериализация часто происходит “на границе” и может быть дорогой.

    Практики:

  • предпочитайте model_dump() как промежуточный шаг, если дальше есть свой JSON-пайплайн
  • используйте exclude_unset=True для PATCH и частичных структур (это ещё и про корректность)
  • включайте by_alias=True только там, где это реально публичный контракт
  • Документация: Pydantic Serialization

    Бенчмаркинг: измеряйте, а не угадывайте

    Оптимизация без измерений часто ухудшает систему.

    Практичный минимум:

  • микробенчмарк на типичных payload
  • отдельные замеры для валидации и для сериализации
  • Для систематических измерений удобны инструменты уровня тестов, например pytest-benchmark.

    Тестирование моделей как контракта

    В продвинутом проекте тестирование Pydantic-моделей — это не “проверить пару полей”, а обеспечить стабильность:

  • входной валидации
  • сериализации
  • схем (JSON Schema/OpenAPI)
  • ошибок (включая коды PydanticCustomError)
  • Юнит-тесты валидаторов и инвариантов

    Тестируйте валидаторы как чистые правила:

  • корректные данные проходят
  • некорректные дают понятную ошибку
  • нормализация действительно нормализует
  • Документация по валидаторам: Pydantic Validators

    Снапшот-тесты сериализации

    Если модели участвуют в публичном контракте, проверяйте model_dump(by_alias=True) снапшотами.

    Что это ловит:

  • случайную смену serialization_alias
  • появление/исчезновение полей
  • изменение форматов после правок сериализаторов
  • Практика:

  • отдельные снапшоты для “внутреннего” model_dump() и для “публичного” model_dump(by_alias=True)
  • Снапшот-тесты JSON Schema

    Из темы про JSON Schema: схема — это контракт.

    Тестируйте:

  • model_json_schema(mode='validation') для входа
  • model_json_schema(mode='serialization') для выхода
  • Документация: Pydantic JSON Schema

    Интеграционные тесты FastAPI: запрос, ответ, OpenAPI

    Если вы используете FastAPI, полезны три уровня интеграционных проверок:

  • запрос с некорректным payload возвращает 422
  • корректный запрос возвращает ответ в формате вашей response_model
  • OpenAPI содержит ожидаемые схемы
  • Документация FastAPI: FastAPI

    Property-based тесты для “широких” входов

    Когда входные данные разнообразны (например, внешние интеграции), хорошо работают property-based тесты, чтобы найти углы, которые вы не предусмотрели.

    Инструмент: Hypothesis

    Архитектурная идея:

  • это особенно полезно для transport DTO, где вы принимаете “грязные” форматы и нормализуете их
  • Миграция Pydantic v1 → v2

    Миграция в v2 — это не только замена методов. Важно сохранить:

  • поведение валидации
  • поведение сериализации
  • схемы (если они публичные)
  • интеграции (FastAPI, ORM, settings)
  • Официальная отправная точка: Pydantic Migration Guide

    Стратегия миграции: снижайте риск по шагам

  • Обновите зависимости так, чтобы окружение поддерживало Pydantic v2.
  • Включите режим совместимости для “старого” кода там, где это нужно, через pydantic.v1.
  • Мигрируйте слой за слоем: модели, валидаторы, сериализация, схемы, интеграции.
  • Зафиксируйте контракт снапшотами: сериализация и JSON Schema.
  • Удалите pydantic.v1, когда кодовая база полностью на v2.
  • Ключевые переименования API

    | Pydantic v1 | Pydantic v2 | |---|---| | parse_obj | model_validate | | parse_raw | model_validate_json | | dict() | model_dump() | | json() | model_dump_json() | | copy() | model_copy() | | construct() | model_construct() | | schema() | model_json_schema() |

    Конфигурация модели: class ConfigConfigDict

    В v1 конфигурация чаще задавалась через вложенный класс Config. В v2 используется model_config = ConfigDict(...).

    Частые соответствия:

  • extra = 'forbid' остаётся как extra='forbid'
  • allow_population_by_field_name = True заменяется на populate_by_name=True
  • orm_mode = True заменяется на from_attributes=True
  • Валидаторы: @validator и @root_validator@field_validator и @model_validator

    В v2 валидаторы стали более явными по стадиям (before, after, wrap).

    Пример миграции валидатора поля:

    Пример миграции root-проверки в инвариант модели:

    Документация: Pydantic Validators

    ORM интеграция: orm_modefrom_attributes

    Если в v1 вы строили схемы из ORM-объектов, в v2 используйте from_attributes=True.

    Это напрямую связано с темой интеграций и проблемой N+1: чтение атрибутов может триггерить ленивые запросы, поэтому отношения нужно загружать заранее.

    Settings: BaseSettings переехал в pydantic-settings

    В v2 настройки приложения вынесены в отдельный пакет.

    Документация: Pydantic Settings

    Быстрая совместимость через pydantic.v1

    Если вы не можете мигрировать всё сразу, в v2 есть модуль совместимости pydantic.v1, который позволяет временно держать часть моделей на старом API.

    Это полезно для поэтапной миграции, но архитектурно важно:

  • фиксируйте границы, где у вас ещё v1-модели
  • не растягивайте этот режим надолго, иначе вы получите две парадигмы в одной кодовой базе
  • Что обязательно проверить после миграции

  • Сериализация публичных DTO: model_dump(by_alias=True).
  • Поведение exclude_unset/exclude_none/exclude_defaults на PATCH и ответах.
  • JSON Schema в режимах validation и serialization, если вы используете разные алиасы.
  • Интеграцию ORM: корректность from_attributes и отсутствие N+1.
  • Интеграцию FastAPI и OpenAPI.
  • Резюме

  • Производительность в Pydantic v2 чаще всего упирается в архитектуру использования: переиспользуйте TypeAdapter, валидируйте JSON напрямую, держите валидаторы и сериализаторы тонкими, а model_construct используйте только для доверенных данных.
  • Тестируйте модели как контракт: юнит-тесты правил, снапшоты сериализации и JSON Schema, интеграционные тесты FastAPI.
  • Миграция v1 → v2 делается безопасно по слоям: конфиги (ConfigDict), методы (model_validate, model_dump), валидаторы (field_validator, model_validator), ORM (from_attributes), settings (pydantic-settings), а затем фиксация схем и публичной сериализации снапшотами.