Урок курса

Модели создания и ответа

FastAPI для начинающих: API с базой данных и тестами

Модель RoomIn описывала данные, которые клиент присылает в теле запроса. У ответа могут быть другие поля: сервер добавляет идентификатор, а внутреннюю информацию оставляет у себя. Раздельные модели позволяют явно описать оба контракта: что принять и что вернуть.

Входная модель RoomIn и модель ответа RoomOut

Когда клиент создаёт комнату, он присылает только то, что знает: название и вместимость. Идентификатор назначает сервер — клиент его не выбирает. Клиент не обязан присылать id, а ответ должен его содержать. Раздельные схемы позволяют выразить эти разные требования напрямую.

Решение — два отдельных класса. RoomIn описывает только то, что клиент присылает:

class RoomIn(BaseModel):
    name: str = Field(..., min_length=2, max_length=50)
    capacity: int = Field(..., ge=1, le=50)

RoomOut описывает то, что сервер возвращает. Здесь уже есть id, который добавит обработчик:

class RoomOut(BaseModel):
    id: int
    name: str
    capacity: int

Взаимосвязь простая: RoomIn — контракт запроса, RoomOut — контракт ответа. Поля, которые клиент не присылает, объявляются только в RoomOut. RoomOut описывает публичные поля ответа. Чтобы FastAPI исключал лишние поля из возвращаемого словаря, эту модель нужно подключить к маршруту через response_model=RoomOut; одного объявления класса недостаточно.

Серверный id: кто его задаёт и почему он не во входной схеме

Поле id объявлено только в RoomOut, потому что его значение назначает обработчик, а не клиент. В обработчике это выглядит так:

def create_room(room: RoomIn):
    record = {"id": 1, "name": room.name, "capacity": room.capacity}
    return record

Значение 1 здесь — демонстрационное. В реальном приложении id приходил бы из базы данных после сохранения; здесь хранения нет, и id фиксирован только чтобы показать механику.

response_model не придумывает id и не подставляет никакого значения по умолчанию. Он получает уже собранный словарь и проверяет его. Если id там есть — хорошо, он попадёт в ответ. Если id отсутствует — FastAPI не придумает его, а вернёт серверную ошибку.

Частая ловушка — объявить id в RoomIn. Тогда Pydantic будет требовать id от клиента и проверять его по схеме. Если обработчик затем напишет {"id": room.id, ...}, он доверяет клиентскому значению — клиент фактически сам назначил себе идентификатор. Само по себе объявление поля в RoomIn не заставляет обработчик его использовать, но соблазн скопировать room.id в результат есть. Чтобы этот сценарий исключить, серверный id не появляется во входной схеме вовсе.

Отдельно: если клиент пришлёт JSON с ключом id, а поля id в RoomIn нет, Pydantic по умолчанию просто проигнорирует этот ключ — ошибки 422 не будет. Но в room.id обратиться не получится, потому что такого атрибута у модели нет.

response_model: проверка ответа и исключение внутреннего поля

response_model подключается в декораторе маршрута. Ниже самостоятельный полный main.py с сокращённой входной моделью. Замените содержимое файла целиком, не добавляйте этот маршрут после старого обработчика:

from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class RoomIn(BaseModel):
    name: str = Field(..., min_length=2, max_length=50)
    capacity: int = Field(..., ge=1, le=50)

class RoomOut(BaseModel):
    id: int
    name: str
    capacity: int

@app.post("/api/rooms", response_model=RoomOut)
def create_room(room: RoomIn):
    record = {"id": 1, "name": room.name, "capacity": room.capacity, "internal_note": "служебное"}
    return record

Обработчик возвращает словарь с четырьмя ключами. FastAPI берёт этот словарь, прогоняет через RoomOut и сериализует только поля, объявленные в классе: id, name, capacity. Поле internal_note в RoomOut не объявлено — в JSON-ответе его не будет.

В /docs отправьте POST /api/rooms с телом {"name": "Лондон", "capacity": 6}. Клиент получит статус 200 и JSON:

{"id": 1, "name": "Лондон", "capacity": 6}

Удалять internal_note из словаря вручную перед return не нужно — response_model работает как белый список.

Два разных типа ошибок. Если клиент прислал некорректные данные — нарушены ограничения RoomIn — FastAPI вернёт 422 Unprocessable Entity ещё до вызова обработчика. Это ошибка входа.

Если обработчик вернул словарь, в котором нет обязательного поля RoomOut (например, забыли добавить id), FastAPI попытается собрать RoomOut из этого словаря и не сможет. Результат — 500 Internal Server Error. Это ошибка ответа: контракт, который сервер обещал, не выполнен. FastAPI не придумывает значения для обязательных полей response_model.

Различие важно: 422 говорит клиенту, что он прислал плохой запрос; 500 — что сервер сломан. При разработке второй сценарий сразу видно в логах, и это правильно: такая ошибка означает баг в обработчике, а не проблему клиента.

Обработчик возвращает словарь с id 1, name Лондон, capacity 6 и internal_note служебное. FastAPI применяет response_model=RoomOut с полями id, name, capacity. JSON-ответ содержит только эти три поля; internal_note исключено. Демонстрационный id 1 задан обработчиком.
RoomOut проверяет результат и оставляет в JSON только свои поля. Демонстрационный id=1 добавлен обработчиком; схема ответа не назначает идентификаторы.

Задача 1

Маршрут объявлен так:

class RoomIn(BaseModel):
    name: str
    capacity: int

class RoomOut(BaseModel):
    id: int
    name: str

@app.post("/api/rooms", response_model=RoomOut)
def create_room(room: RoomIn):
    return {"id": 5, "name": room.name, "capacity": room.capacity}

Клиент отправляет {"name": "Токио", "capacity": 8}. Что окажется в теле JSON-ответа?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

Разработчик хочет, чтобы поле access_token никогда не возвращалось клиенту. Он добавил его в словарь внутри обработчика для внутренней логики. Какой способ правильно исключает это поле из JSON-ответа?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

RoomOut объявляет обязательные поля id: int и name: str. Маршрут использует response_model=RoomOut, но обработчик возвращает только {"name": "Зал"} — поле id отсутствует. Входной запрос корректен. Что произойдёт при стандартной обработке ответа FastAPI?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 4

Публичная карточка Roomly

Офис-менеджер описывает переговорную комнату и передаёт сведения о ней в Roomly, чтобы получить аккуратно оформленную публичную карточку. Сервис принимает описание, проверяет его корректность и возвращает результат, в котором переданные сведения дополнены идентификатором — его присваивает сам сервис, а не отправитель.

Пока сервис готовит ответ, внутри его работы существует служебная пометка: она нужна самому сервису в момент обработки, однако получателю карточки она не предназначена. Разграничение здесь принципиально: то, что видит офис-менеджер, и то, что циркулирует внутри системы, — это разные вещи, и публичная карточка не должна раскрывать ничего лишнего.

Вместе с тем правила проверки описания остаются в силе: если переданные сведения содержат ошибку, успешной карточки не последует. Когда же описание корректно, офис-менеджер получает именно то, что ему нужно, — чистую карточку с информацией о комнате и присвоенным идентификатором, без каких-либо внутренних пометок сервиса.

Важно понимать, что этот результат носит предварительный характер: каталог переговорных не меняется, никакие записи не создаются и не сохраняются, а идентификатор в карточке лишь демонстрирует, как он мог бы выглядеть. Цель — убедиться, что публичное представление комнаты формируется правильно и не содержит ничего, что должно оставаться за кадром.

Что уже дано

Дан самостоятельный файл с app, готовыми Address и RoomIn, а также обработчиком POST /api/rooms. Входная валидация уже работает. Обработчик задаёт демонстрационный id=101 и возвращает словарь, содержащий служебное internal_note. Сейчас оно попадает к клиенту. Дополните файл моделью ответа и подключите её к существующему маршруту; второй маршрут на тот же путь не добавляйте.

Служебный переходник проверки запускается перед вашим файлом, но не создаёт приложение или данные. Менять его не нужно.

Что нужно сделать

Объявите RoomOut на основе BaseModel и подключите её через response_model. Публичный ответ со статусом 200 должен содержать ровно id, name, capacity, вложенный address с city и street, а также comment — строку или null. Обязательные поля RoomOut: id, name, capacity, address; для comment допустим None по умолчанию. Поля internal_note в публичной модели быть не должно.

Сохраните входной контракт RoomIn: обязательное имя длиной 2–50 символов, обязательная вместимость 1–50, обязательный вложенный адрес с двумя строками и необязательный комментарий. Входные ошибки остаются стандартными 422, включая путь к неверному вложенному полю. Клиентский лишний id не должен подменять серверный: результат всегда содержит демонстрационное число 101.

Служебная пометка остаётся в возвращаемом обработчиком словаре; настройте исключение поля через модель ответа. Данные не сохраняются, счётчик не нужен: число 101 не обещает уникальность и не означает создание комнаты.

Ввод и вывод

stdin не используется. Печатать через print в stdout ничего не нужно. После выполнения файла проверяющая система обращается к верхнеуровневому объекту app: служебный переходник отправляет HTTP-запросы через TestClient внутри процесса приложения и передаёт результаты скрытой проверке. Запускать Uvicorn или сетевой сервер не нужно.

POST /api/rooms с {"name": "Вяз", "capacity": 8, "address": {"city": "Омск", "street": "Садовая"}} получает 200 и {"id": 101, "name": "Вяз", "capacity": 8, "address": {"city": "Омск", "street": "Садовая"}, "comment": null}. Служебной пометки в ответе нет.

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задачи к уроку

Все задачи курса →

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку