Query-параметры, значения по умолчанию и Annotated

Query-параметры — это часть URL после ?, которую FastAPI распознаёт по параметрам функции, не вошедшим в путь, и валидирует так же, как всё остальное.

Правило простое: параметр функции, имя которого не встречается в пути, FastAPI считает query-параметром. Есть значение по умолчанию — он опциональный, нет — обязательный.

Query-строка нужна для фильтрации, пагинации, сортировки, поиска — всего, что уточняет запрос, но не идентифицирует ресурс. В URL /items?skip=0&limit=20&q=phone три query-параметра. FastAPI различает path и query по простому признаку: если имя параметра функции есть в шаблоне пути — это path, если нет — query.

from fastapi import FastAPI, Query
from typing import Annotated

app = FastAPI()

@app.get("/items")
async def list_items(
    skip: int = 0,
    limit: Annotated[int, Query(le=100)] = 20,
    q: str | None = None,
):
    return {"skip": skip, "limit": limit, "q": q}

Здесь skip имеет дефолт 0 (опциональный), limit по умолчанию 20 и не больше 100, а q может отсутствовать (str | None = None). Современный синтаксис ограничений — снова Annotated[int, Query(...)]; он отделяет тип от метаданных и при этом сохраняет настоящее значение по умолчанию справа от =.

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

Браузер и клиенты передают query как плоскую строку key=value&key2=value2, где всё — текст и всё URL-кодировано. FastAPI разбирает её, сопоставляет с параметрами функции, конвертирует типы и подставляет дефолты. Разберём query-строку вручную на stdlib — это ровно та работа, которую делает фреймворк до вызова обработчика:

from urllib.parse import parse_qs

raw = "skip=10&limit=50&q=phone%20case"
parsed = parse_qs(raw)            # {'skip': ['10'], 'limit': ['50'], 'q': ['phone case']}
print("сырой разбор:", parsed)

def get(name, default=None, cast=str):
    if name not in parsed:
        return default            # подстановка значения по умолчанию
    return cast(parsed[name][0])  # берём первое значение и приводим тип

skip = get("skip", 0, int)
limit = get("limit", 20, int)
q = get("q", None, str)
print("skip =", skip, "| limit =", limit, "| q =", repr(q))

# проверка ограничения le=100
if limit > 100:
    print("422: limit превышает 100")
else:
    print("итог:", {"skip": skip, "limit": limit, "q": q})

Попробуй сам ▶ Поменяй raw: убери q — увидишь, как срабатывает значение по умолчанию None.

Заметьте, что %20 декодировалось в пробел: query всегда URL-кодирована, и декодирование — тоже забота FastAPI.

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

Первая — путать «нет дефолта» и «дефолт None». Без значения по умолчанию параметр обязателен, и его отсутствие даст 422. Вторая — ожидать, что булев query-параметр придёт как Python-bool сам по себе: FastAPI понимает true/false/1/0/yes/no, но только потому, что вы аннотировали тип bool. Третья — складывать сложные структуры в один query-параметр строкой вместо нормальной модели или повторяющихся ключей (списков).

Best practices

  • Опциональность выражайте значением по умолчанию; обязательность — его отсутствием.
  • Ограничения (ge, le, max_length, pattern) задавайте через Annotated[..., Query(...)].
  • Для пагинации используйте skip/limit или page/size с разумными дефолтами и верхней границей.
  • Списочные query-параметры объявляйте как list[str] — FastAPI соберёт повторяющиеся ключи.

Списки и сложные query-параметры

Query-строка умеет больше, чем плоские пары. Если объявить параметр как tags: list[str], FastAPI соберёт все повторяющиеся ключи: запрос ?tags=a&tags=b&tags=c даст список из трёх элементов. Это стандартный способ передавать множественные фильтры. Когда же параметров много и они логически связаны, их группируют в Pydantic-модель и помечают как query-зависимость — тогда вместо десятка отдельных аргументов в сигнатуре появляется один типизированный объект фильтра. Важно не злоупотреблять: складывать в один строковый query-параметр сериализованный JSON — антипаттерн, который ломает кэширование, логи и документацию. Если структура сложная, ей место в теле запроса (для операций, где тело уместно) или в нескольких явных параметрах, а не в одной перегруженной строке.

Итог: query-параметр — это параметр функции вне пути; дефолт делает его опциональным. FastAPI декодирует, конвертирует и валидирует query-строку до вызова обработчика.

Проверьте себя
1. Как FastAPI отличает query-параметр от path-параметра?
AПо типу данных
BПо наличию слова 'query' в имени
CЕсли имя параметра функции есть в шаблоне пути — это path, если нет — query
DQuery-параметры всегда строки, path — числа
2. Параметр объявлен как q: str (без значения по умолчанию). Каким он будет?
AОпциональным со значением None
BОбязательным — его отсутствие вызовет ошибку 422
CPath-параметром
DЗаголовком