Динамические маршруты, валидация параметров, типы данных
Параметры пути позволяют создавать динамические маршруты вроде
/users/{user_id}. В этой теме вы научитесь валидировать параметры, накладывать ограничения и использовать продвинутые возможности.
Параметры пути объявляются в фигурных скобках:
from fastapi import FastAPI
app = FastAPI()
@app.get('/users/{user_id}')
def get_user(user_id: int):
return {'user_id': user_id}При запросе /users/5 вы получите:
{"user_id": 5}{user_id} в путиuser_id в сигнатуре функцииint)FastAPI поддерживает любые Python-типы:
@app.get('/items/{item_id}')
def get_item(item_id: int):
return {'type': 'int', 'value': item_id}
@app.get('/products/{product_id}')
def get_product(product_id: float):
return {'type': 'float', 'value': product_id}
@app.get('/flags/{flag_id}')
def get_flag(flag_id: bool):
return {'type': 'bool', 'value': flag_id}| Тип | Примеры URL | Результат |
|---|---|---|
int | /items/42 | 42 |
float | /products/3.14 | 3.14 |
bool | /flags/true, /flags/False | True, False |
str | /names/alex | "alex" |
FastAPI распознаёт следующие значения как True:
"true", "True", "TRUE", "1", "yes", "on"Как False:
"false", "False", "FALSE", "0", "no", "off"Запрос /flags/yes → flag_id = True
Иногда нужно больше, чем просто проверка типа. Например:
Для этого используется Path():
from fastapi import FastAPI, Path
app = FastAPI()
@app.get('/items/{item_id}')
def get_item(item_id: int = Path(..., title="ID элемента", description="Уникальный идентификатор")):
return {'item_id': item_id}Path()?... (Ellipsis) — означает, что параметр обязателен (нельзя опустить)title — заголовок для документацииdescription — подробное описаниеМожно использовать аннотацию Annotated (Python 3.10+):
from typing import Annotated
from fastapi import FastAPI, Path
app = FastAPI()
@app.get('/items/{item_id}')
def get_item(item_id: Annotated[int, Path(title="ID элемента", description="Уникальный идентификатор")]):
return {'item_id': item_id}Этот синтаксис предпочтителен в Pydantic v2 и новых версиях FastAPI.
from fastapi import FastAPI, Path
app = FastAPI()
@app.get('/items/{item_id}')
def get_item(
item_id: int = Path(
...,
title="ID элемента",
ge=1, # больше или равно (greater or equal)
le=1000, # меньше или равно (less or equal)
gt=0, # строго больше (greater than)
lt=1001, # строго меньше (less than)
)
):
return {'item_id': item_id}| Параметр | Значение | Проверка |
|---|---|---|
ge | 1 | item_id >= 1 |
le | 1000 | item_id <= 1000 |
gt | 0 | item_id > 0 |
lt | 1001 | item_id < 1001 |
Запрос /items/0 вернёт:
{
"detail": [
{
"type": "greater_than_equal",
"loc": ["path", "item_id"],
"msg": "Input should be greater than or equal to 1",
"input": "0",
"ctx": {"ge": 1}
}
]
}from fastapi import FastAPI, Path
app = FastAPI()
@app.get('/users/{username}')
def get_user(
username: str = Path(
...,
min_length=3,
max_length=50,
pattern=r'^[a-zA-Z0-9_]+$', # только буквы, цифры, подчёркивание
)
):
return {'username': username}| Параметр | Значение | Проверка |
|---|---|---|
min_length | 3 | Длина строки >= 3 |
max_length | 50 | Длина строки <= 50 |
pattern | regex | Строка должна соответствовать шаблону |
pattern=r'^[a-zA-Z0-9_]+$' означает:
^ — начало строки[a-zA-Z0-9_]+ — одна или более букв (любой регистр), цифр или подчёркивания$ — конец строкиЗапрос /users/john_doe123 → OK
Запрос /users/john-doe → Ошибка 422 (дефис не разрешён)
@app.get('/users/{user_id}/posts/{post_id}')
def get_user_post(
user_id: int = Path(..., ge=1),
post_id: int = Path(..., ge=1)
):
return {'user_id': user_id, 'post_id': post_id}Запрос /users/5/posts/42 вернёт:
{"user_id": 5, "post_id": 42}Параметры в пути должны объявляться в том же порядке, в котором они встречаются в URL:
# Правильно:
@app.get('/a/{a}/b/{b}/c/{c}')
def handler(a: int, b: int, c: int): ...
# Неправильно (работает, но запутанно):
@app.get('/a/{a}/b/{b}/c/{c}')
def handler(c: int, a: int, b: int): ... # Python сопоставит по имени, но это плохой стильИногда нужно ограничить набор допустимых значений:
from enum import Enum
from fastapi import FastAPI
class ModelName(str, Enum):
alexnet = "alexnet"
resnet = "resnet"
squeezenet = "squeezenet"
app = FastAPI()
@app.get('/models/{model_name}')
def get_model(model_name: ModelName):
if model_name == ModelName.alexnet:
return {"model_name": model_name, "message": "Deep Learning CNN"}
if model_name == ModelName.resnet:
return {"model_name": model_name, "message": "Residual Network"}
return {"model_name": model_name, "message": "Compact Network"}str, Enum?Класс должен наследовать и str, и Enum:
str — позволяет FastAPI обрабатывать значение как строку (для URL и JSON)Enum — даёт возможность перечислять допустимые значения/models/alexnet значение преобразуется в ModelName.alexnetЗапрос /models/vgg вернёт:
{
"detail": [
{
"type": "enum",
"loc": ["path", "model_name"],
"msg": "Input should be 'alexnet', 'resnet' or 'squeezenet'",
"input": "vgg",
"ctx": {"expected": "'alexnet', 'resnet', 'squeezenet'"}
}
]
}Можно комбинировать статические и динамические части:
@app.get('/users/me')
def get_current_user():
return {"user_id": "current", "username": "john"}
@app.get('/users/{user_id}')
def get_user(user_id: int):
return {"user_id": user_id}Важно: Статические маршруты должны объявляться перед динамическими!
FastAPI проверяет маршруты в порядке объявления:
/users/me → точное совпадение/users/{user_id} → шаблон для остальныхЕсли поменять порядок, /users/me попадёт в /users/{user_id} и user_id попытается преобразоваться в int из строки "me" → ошибка 422.
Иногда нужно получить весь путь целиком, включая слеши:
from fastapi import FastAPI
app = FastAPI()
@app.get('/files/{file_path:path}')
def get_file(file_path: str):
return {"file_path": file_path}Суффикс :path говорит FastAPI: «захвати всё до конца пути».
Запрос /files/home/user/documents/file.txt вернёт:
{"file_path": "home/user/documents/file.txt"}Без :path FastAPI попытается сопоставить только первый сегмент (home) и вернёт 404 для остального.
Создадим полноценное API для блога с параметрами пути:
from fastapi import FastAPI, Path
from typing import Annotated
app = FastAPI(title="Blog API")
# Получить статью по ID
@app.get('/posts/{post_id}')
def get_post(
post_id: Annotated[int, Path(..., ge=1, description="ID статьи")]
):
return {
"post_id": post_id,
"title": f"Post {post_id}",
"content": "Content here..."
}
# Получить комментарий по ID поста и ID комментария
@app.get('/posts/{post_id}/comments/{comment_id}')
def get_comment(
post_id: Annotated[int, Path(..., ge=1)],
comment_id: Annotated[int, Path(..., ge=1)]
):
return {
"post_id": post_id,
"comment_id": comment_id,
"text": f"Comment {comment_id} on post {post_id}"
}
# Получить пользователя по username
@app.get('/users/{username}')
def get_user(
username: Annotated[str, Path(..., min_length=3, max_length=50, pattern=r'^[a-zA-Z0-9_]+$')]
):
return {
"username": username,
"name": f"User {username}"
}
# Получить файл по пути
@app.get('/files/{file_path:path}')
def get_file(file_path: str):
return {"file_path": file_path, "content": "..."}Откройте /docs и найдите ваш эндпоинт. Вы увидите:
Path()Нажмите "Try it out", введите значение → Execute.
Если валидация не пройдёт, увидите детальную ошибку.
Добавьте логирование для отладки:
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
@app.get('/items/{item_id}')
def get_item(item_id: int = Path(..., ge=1)):
logger.debug(f"Received item_id: {item_id}, type: {type(item_id)}")
return {'item_id': item_id}В логах увидите:
DEBUG:__main__:Received item_id: 42, type: <class 'int'>
@app.get('/users/{user_id}')
def get_user(uid: int): # Ошибка: имя не совпадает!
...Проблема: Параметр пути user_id не найден в функции (там uid).
Решение: Имена должны совпадать:
@app.get('/users/{user_id}')
def get_user(user_id: int): ...@app.get('/a/{a}/b/{b}')
def handler(b: int, a: int): # Запутанный код
...Проблема: Python сопоставит по имени, но это сбивает с толку.
Решение: Объявляйте в том же порядке:
@app.get('/a/{a}/b/{b}')
def handler(a: int, b: int): ...... в Path()@app.get('/items/{item_id}')
def get_item(item_id: int = Path(ge=1)): # Ошибка: нет обязательного значения
...Проблема: Без ... или значения по умолчанию FastAPI не поймёт, обязателен ли параметр.
Решение:
@app.get('/items/{item_id}')
def get_item(item_id: int = Path(..., ge=1)): ...@app.get('/users/{username}')
def get_user(username: str = Path(..., pattern=r'^[a-z]+$')):
...Запрос /users/John → ошибка, хотя кажется, что подходит.
Проблема: Regex чувствителен к регистру.
Решение:
pattern=r'^[a-zA-Z]+$' # или флаг re.IGNORECASE{param_name}:pathВ следующей теме вы изучите query-параметры — параметры в строке запроса (?skip=0&limit=10), которые используются для фильтрации, пагинации и сортировки.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.