Тело запроса, методы и коды статусов
Тело запроса описывается 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 и валидирует. Метод выражает намерение операции, код статуса — её результат.