Урок курса

Ограничения полей и ошибки валидации

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

В прошлом уроке модель RoomIn объявляла поля через простые аннотации: name: str и capacity: int. FastAPI проверял тип, но ничего не знал о допустимых значениях — вместимость 0 или имя из одного символа проходили без ошибки. Сейчас добавим правила прямо в объявление поля.

Field(...) как контракт: ge, le, min_length, max_length

Чтобы задать числовые или строковые ограничения, используют Field из pydantic. Выражение с Field записывают справа от =: оно описывает ограничения поля и, при необходимости, его значение по умолчанию. В показанном ниже Field(...) многоточие оставляет поле обязательным без значения по умолчанию.

Первый аргумент Field — это либо многоточие ..., либо конкретное значение по умолчанию. Многоточие означает «поле обязательно»: если клиент не передаст его в теле запроса, FastAPI вернёт 422 не выполняя обработчик.

Для числовых полей:

  • ge — значение должно быть больше или равно указанному;
  • le — меньше или равно;
  • gt и lt — строго больше и строго меньше.

Для строковых:

  • min_length — минимальная длина в символах Unicode;
  • max_length — максимальная.

Модель для переговорной выглядит так:

from pydantic import BaseModel, Field

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

Теперь name обязана содержать от 2 до 50 символов, а capacity — целое число от 1 до 50 включительно. Правила проверяются до того, как управление дойдёт до функции-обработчика: если хотя бы одно нарушено, FastAPI отвечает 422.

Field(0, ge=1) против Field(..., ge=1): что происходит при пропуске поля

Представьте, что кто-то написал модель чуть иначе:

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

Теперь у capacity есть значение по умолчанию — 0. Если клиент отправит тело без этого поля:

{"name": "Большой зал"}

Pydantic v2 в стандартной конфигурации подставит 0 и не будет проверять, удовлетворяет ли сам default ограничению ge=1. Обработчик получит capacity=0 и вернёт 200.

При этом если клиент явно передаст "capacity": 0, ограничение сработает и ответ будет 422. Итог: одно и то же значение 0 ведёт себя по-разному в зависимости от того, пришло ли оно из запроса или было подставлено как default.

Чтобы этого не было, используйте Field(..., ge=1). Тогда пропущенное поле даёт 422, а default, нарушающий правило, в принципе не возникает. Значение по умолчанию стоит задавать только тогда, когда оно само по себе допустимо по контракту.

Как прочитать ответ 422 и найти неверное поле

Возьмём полный маршрут с моделью, которую объявили выше:

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)

@app.post("/api/rooms")
def create_room(room: RoomIn):
    return {"received_name": room.name, "capacity": room.capacity}

Отправим запрос с намеренно неверными значениями:

{"name": "А", "capacity": 0}

FastAPI вернёт статус 422. Ниже сокращённое тело ответа: у каждой ошибки оставлены loc, msg и type.

{
  "detail": [
    {
      "loc": ["body", "name"],
      "msg": "String should have at least 2 characters",
      "type": "string_too_short"
    },
    {
      "loc": ["body", "capacity"],
      "msg": "Input should be greater than or equal to 1",
      "type": "greater_than_equal"
    }
  ]
}

detail — массив: каждый объект описывает одно нарушение. Для диагностики рассмотрим три показанных поля; в полном ответе могут быть и другие, например input и ctx.

loc — путь к проблемному месту. Первый элемент "body" говорит, что ошибка в JSON-теле запроса. Второй — имя конкретного поля модели. По этому пути сразу понятно, что исправлять.

msg — человекочитаемое описание: что именно нарушено. Видно, что name слишком короткое, а capacity меньше 1.

type — машинный код ошибки: удобен, если нужно обрабатывать ответ программно.

В данном случае оба поля нарушены одновременно, поэтому detail содержит два объекта. Чтобы исправить запрос, достаточно передать name длиной от 2 до 50 символов и capacity от 1 до 50 — например, {"name": "Малый зал", "capacity": 10}.

Поле capacity: int ограничено Field(..., ge=1, le=50). Переданное capacity 0 даёт 422 до вызова обработчика. В одном элементе detail loc равен [body, capacity], а msg сообщает Input should be greater than or equal to 1.
loc указывает поле capacity, msg объясняет нарушенное правило: значение должно быть не меньше 1. Обработчик не вызывается.

Попробуйте решить

Модель объявлена так:

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

Клиент отправляет {"name": "Переговорная номер один в офисе Москва длинное название которое точно превышает лимит", "capacity": 10}. Каков статус ответа и какое поле укажет loc?

Продолжить с проверкой и прогрессом

Откройте интерактивный раннер с заданиями урока.

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