Тело запроса, методы и коды статусов

Тело запроса описывается Pydantic-моделью в параметре функции; HTTP-метод задаётся декоратором (post/put/patch/delete), а код статуса — параметром status_code.

Если параметр функции — это Pydantic-модель, FastAPI читает его из тела запроса (JSON), валидирует и передаёт вам готовый объект. Метод и код статуса вы выбираете осознанно по семантике операции.

GET и DELETE обычно не несут тела — они идентифицируют ресурс через путь. А вот создание и изменение требуют данных, и эти данные едут в теле запроса в формате JSON. В FastAPI вы не разбираете JSON руками: вы объявляете Pydantic-модель, и параметр такого типа автоматически читается из тела.

from fastapi import FastAPI, status
from pydantic import BaseModel

app = FastAPI()

class ItemCreate(BaseModel):
    name: str
    price: float
    in_stock: bool = True

@app.post("/items", status_code=status.HTTP_201_CREATED)
async def create_item(item: ItemCreate):
    return {"created": item.name, "price": item.price}

Семантика методов: POST — создать ресурс, PUT — заменить целиком, PATCH — частично обновить, DELETE — удалить, GET — получить. Код статуса сообщает результат: 200 — успех, 201 — создано, 204 — успех без тела, 404 — не найдено, 422 — ошибка валидации. Здесь мы явно вернули 201 для создания.

Как работает под капотом

FastAPI читает сырое тело, парсит JSON и отдаёт его Pydantic-модели на валидацию. Если данных не хватает или они не того типа — 422 ещё до вашего кода. Смоделируем разбор тела и проверку обязательных полей на stdlib:

import json

def validate_item(raw_body):
    data = json.loads(raw_body)           # парсим JSON-тело
    errors = []
    if "name" not in data:
        errors.append("поле 'name' обязательно")
    if "price" not in data:
        errors.append("поле 'price' обязательно")
    else:
        try:
            data["price"] = float(data["price"])  # конвертация типа
        except (TypeError, ValueError):
            errors.append("'price' должно быть числом")
    data.setdefault("in_stock", True)     # значение по умолчанию
    if errors:
        return 422, {"detail": errors}
    return 201, {"created": data["name"], "price": data["price"]}

print(validate_item('{"name": "Чехол", "price": "990"}'))
print(validate_item('{"name": "Кабель"}'))
print(validate_item('{"price": 100}'))

Попробуй сам ▶ Это и есть конвейер «парсинг → проверка обязательных полей → конвертация типов → дефолты», который Pydantic делает на промышленном уровне.

Полный конвейер запроса

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

HTTP POST /items   { "name": "...", "price": ... }
        |
        v
[ роутинг: метод + путь -> обработчик ]
        |
        v
[ разбор источников: path / query / headers / body ]
        |
        v
[ Pydantic: валидация тела по модели ItemCreate ]
        |
        +-- ошибка --> 422 { "detail": [ ... путь до поля ... ] }
        |
        v
[ разрешение зависимостей (Depends), кэш на запрос ]
        |
        v
[ ваш обработчик: бизнес-логика ]
        |
        v
[ Pydantic: сериализация по response_model ]
        |
        v
HTTP-ответ  (status_code, JSON-тело)

Понимание этого конвейера снимает большинство вопросов «почему пришла 422, хотя обработчик не вызвался»: значит, запрос отсеялся на этапе валидации тела, ещё до вашего кода. И наоборот, ошибка внутри обработчика — это уже 500 или ваш HTTPException, потому что валидация уже пройдена.

Частые ошибки

Первая — пытаться отправить тело в GET-запросе; многие клиенты и прокси его игнорируют, да и семантически это неверно. Вторая — путать PUT и PATCH: PUT заменяет ресурс целиком (отсутствующие поля обнуляются по контракту), PATCH меняет только переданное. Третья — возвращать код 200 на создание вместо 201, теряя точную семантику. Четвёртая — вручную делать json.loads(request.body) вместо объявления модели, отказываясь от валидации.

Best practices

  • Описывайте тело Pydantic-моделью — получаете валидацию и документацию бесплатно.
  • Подбирайте метод по семантике: POST — создать, PUT — заменить, PATCH — частично обновить.
  • Указывайте осмысленный status_code: 201 при создании, 204 при удалении без тела.
  • Разделяйте модели входа и выхода (об этом — в разделе про Pydantic).

Несколько тел и параметр Body

Иногда в одном запросе нужно принять не одну модель, а несколько именованных объектов плюс отдельные скалярные значения. FastAPI это умеет: если объявить два параметра-модели, он будет ожидать тело с двумя ключами верхнего уровня, по имени каждого параметра. А чтобы скалярное значение (например, importance: int) тоже пришло из тела, а не из query, его помечают маркером Body(). Это тонкий, но частый источник путаницы: по умолчанию простой тип — это query, и только явный Body() переносит его в тело. Понимание этого правила убирает загадки вроде «почему моё число ищется в URL, хотя я шлю его в JSON». Правило короткое: модель — всегда тело; простой тип — по умолчанию query, в тело его переводит Body().

Итог: тело запроса — это Pydantic-модель-параметр, которую FastAPI читает из JSON и валидирует. Метод выражает намерение операции, код статуса — её результат.

Проверьте себя
1. Откуда FastAPI берёт значение параметра, если он аннотирован Pydantic-моделью?
AИз query-строки
BИз заголовков
CИз тела запроса (JSON), парсит и валидирует его
DИз cookies
2. Какой код статуса семантически правильно вернуть при успешном создании ресурса через POST?
A200 OK
B201 Created
C204 No Content
D302 Found