Урок курса
Модели создания и ответа
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 — что сервер сломан. При разработке второй сценарий сразу видно в логах, и это правильно: такая ошибка означает баг в обработчике, а не проблему клиента.

Попробуйте решить
Маршрут объявлен так:
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-ответа?
Продолжить с проверкой и прогрессом
Откройте интерактивный раннер с заданиями урока.
