Перейти к содержанию

Модели Pydantic для запроса и ответа

Оглавление серии

Каталог уже возвращает кабинет по номеру. Следующая задача «Окна» — принять предполагаемый интервал, не сохраняя его. Такой предварительный просмотр позволяет изучить форму запроса отдельно от записи в базу. Мы введём DTO: объект, описывающий данные на границе системы. DTO не обязан совпадать с таблицей, поскольку клиент создаёт намерение, а сервер добавляет идентификатор, владельца и статус.

В этом состоянии используются модели Pydantic второго семейства. Называем классы BookingIn, BookingOut и ResourceOut по направлению данных. Это обозначение удобно при чтении обработчика: видно, что принимает клиент и что обещает сервер. Предварительный маршрут возвращает BookingIn, поэтому пока не изображает полноценную созданную бронь. Настоящий результат создания появится после подключения хранилища, и ожидаемый статус этого учебного просмотра остаётся обычным успешным ответом.

Поля и предметная проверка

resource_id — положительное целое число. Ограничение gt=0 проверяет форму идентификатора, но не доказывает существование кабинета. Кабинет с номером 500 проходит такую проверку, если число положительно, и требует отдельного поиска. Это хороший пример разных уровней правил: схема запроса знает тип и допустимый диапазон, а предметная операция обращается к каталогу. Не помещаем запрос к базе внутрь простого валидатора поля.

starts_at и ends_at описывают начало и конец. Пока обе даты могут быть без смещения или обе со смещением. Смешанный вариант отвергается, чтобы Python не сравнивал наивную и aware-дату. Такой промежуточный договор явно ограничен: отсутствие часового пояса ещё допустимо, поэтому состояние нельзя применять к расписанию нескольких регионов. В отдельной главе перейдём к обязательному смещению и нормализации UTC. Это изменение будет касаться именно времени, а не остальных полей.

Проверка после создания модели сравнивает два уже разобранных значения. Если конец не следует за началом, возникает ошибка ввода. Здесь важна строгая граница: равные значения также запрещены. Нулевой интервал не занимает кабинет и плохо подходит для предметного события. Преимущество модели в том, что проверка записана в одном месте и действует при разных способах построения BookingIn, а обработчик предварительного просмотра остаётся коротким.

Полные файлы шага

Показанные файлы полностью совпадают с выбранным состоянием архива. Остальные согласованные файлы находятся в examples/fastapi-booking-lab/lesson-02/.

schemas.py

from datetime import datetime
from typing import Literal

from pydantic import BaseModel, ConfigDict, Field, model_validator


class ResourceOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    name: str


class BookingIn(BaseModel):
    model_config = ConfigDict(extra="forbid")
    resource_id: int = Field(gt=0)
    starts_at: datetime
    ends_at: datetime

    @model_validator(mode="after")
    def valid_interval(self):
        if (self.starts_at.utcoffset() is None) != (self.ends_at.utcoffset() is None):
            raise ValueError("Use the same datetime awareness for both endpoints")
        if self.ends_at <= self.starts_at:
            raise ValueError("End must follow start")
        return self


class BookingOut(BookingIn):
    model_config = ConfigDict(from_attributes=True, extra="ignore")
    id: int
    actor_id: int
    state: Literal["confirmed", "cancelled"] = "confirmed"

Ожидаемый результат

Это ожидаемый договор для самостоятельного исследования; пример не выполнялся.

POST /booking-preview, конец позже начала → 200
POST /booking-preview, конец равен началу → 422
Неизвестное поле actor_id во входе → 422

Что клиенту запрещено назначать

Параметр extra="forbid" отклоняет неизвестные свойства во входном объекте. Представьте запрос, где рядом с resource_id читатель добавляет actor_id=2. Сейчас владельца вообще нельзя передавать в намерении. Если бы модель молча принимала неизвестные значения, человек мог решить, что это поле поддерживается. Явный отказ лучше показывает контракт: пользователь выбирает ресурс и время, а принадлежность записи определяется сервером в отдельной главе о входе.

BookingOut наследует поля интервала, но добавляет id, actor_id и state. Это не разрешение клиенту заполнять выходную модель. Класс нужен для представления серверного результата. Статус ограничен двумя строками: confirmed и cancelled. Список допустимых значений делает договор понятнее свободной строки. При ошибочном состоянии серверный ответ должен считаться нарушением реализации, а не ошибкой пользователя, поскольку пользователь такой статус здесь не задаёт.

ResourceOut описывает кабинет двумя полями. from_attributes позволяет читать свойства объекта, когда позднее вместо словаря появится ORM-экземпляр. Обратите внимание: настройка не создаёт таблицу, не подключает базу и не загружает связанные данные. Она меняет способ получения значений при валидации модели. Благодаря этому внешний ответ может сохраниться, хотя внутреннее представление ресурса поменяется. Клиентский договор отделяется от устройства хранения.

Одна и та же форма на входе и выходе

Передайте в предварительный просмотр кабинет 1 и часовой интервал 15 ноября 2026 года. Ожидается ответ с теми же предметными полями. Для положительного сценария выберите конец позже начала. Затем замените конец на начало: ожидается ошибка валидации до выполнения функции preview_booking. Третий вариант — добавьте неизвестное свойство comment: модель его отвергнет, хотя JSON сам по себе синтаксически правильный. Синтаксис JSON и допустимость запроса являются разными вопросами.

Возвращаемый объект также проходит через response_model. Для карточек каталога это ResourceOut, поэтому структура ответа заявлена явно. Если обработчик позднее возвращает объект с дополнительной технической информацией, внешняя модель задаёт перечень выдаваемых полей. Однако не рассматривайте фильтрацию как единственную защиту секретов: внутренний объект вообще не должен случайно собирать чужие данные. DTO помогает удержать границу, а доступ к данным решается раньше, в предметной операции.

Метод model_dump создаёт словарь модели, а model_dump(mode="json") готовит значения, пригодные для JSON-представления. В этом уроке метод ещё не нужен маршруту: FastAPI умеет обработать возвращённую модель. Мы называем его сейчас, потому что дальше той же входной структурой будем наполнять ORM-объект. Не копируем в запись сырое тело целиком: сначала оно должно пройти заявленные типы, ограничения и проверку интервала.

Полный schemas.py на этой странице совпадает со снимком урока. В main.py добавлен предварительный POST и response_model у каталожных маршрутов; весь файл находится в архиве. При самостоятельном переносе берите целый lesson-02, поскольку старый main.py не импортирует новые модели. Полученный результат — не бронирование кабинета, а устойчивое описание намерения и внешней карточки. Теперь сможем договориться о форме отказа, не меняя успешные данные.

Преобразование не заменяет строгость договора

Валидация Pydantic может преобразовывать некоторые входные значения к объявленному типу. Поэтому аннотация int не должна автоматически толковаться как запрет любой строки во всех режимах. Для этого существуют отдельные средства строгой проверки. В «Окне» важен полученный положительный идентификатор, и мы не объявляем весь ввод strict. Если продукту понадобится различать число 1 и строку с цифрой, такое требование надо выразить в модели явно и затем обновить документацию клиента.

Наследование BookingOut от BookingIn также требует внимания. Выходная модель сохраняет предметную проверку интервала, поскольку ответ должен описывать допустимую запись. Но у неё другая настройка дополнительных полей: внутренний ORM-объект может иметь служебные свойства, которые не входят в JSON. Это не расширяет входной контракт. Рассмотрите две разные задачи: отвергнуть неизвестное свойство намерения и спроецировать внутренний результат на заявленный набор полей. Они относятся к разным направлениям данных, поэтому одну универсальную модель для всех целей мы не используем.

Документация и исходники

Механизмы главы сверены с первичными источниками: документация 1, документация 2, документация 3. Полный архив «Окна» содержит все шестнадцать состояний. Пакеты, приложение и миграции не запускались.