Перейти к содержанию

Расширенная конфигурация операций пути

OpenAPI operationId

Предупреждение

Если вы не «эксперт» по OpenAPI, скорее всего, это вам не нужно.

Вы можете задать OpenAPI operationId, который будет использоваться в вашей операции пути, с помощью параметра operation_id.

Нужно убедиться, что он уникален для каждой операции.

from fastapi import FastAPI

app = FastAPI()


@app.get("/items/", operation_id="some_specific_id_you_define")
async def read_items():
    return [{"item_id": "Foo"}]

Использование имени функции-обработчика пути как operationId

Если вы хотите использовать имена функций ваших API в качестве operationId, вы можете пройти по всем из них и переопределить operation_id каждой операции пути с помощью их APIRoute.name.

Делать это следует после добавления всех операций пути.

from fastapi import FastAPI
from fastapi.routing import APIRoute

app = FastAPI()


@app.get("/items/")
async def read_items():
    return [{"item_id": "Foo"}]


def use_route_names_as_operation_ids(app: FastAPI) -> None:
    """
    Simplify operation IDs so that generated API clients have simpler function
    names.

    Should be called only after all routes have been added.
    """
    for route in app.routes:
        if isinstance(route, APIRoute):
            route.operation_id = route.name  # in this case, 'read_items'


use_route_names_as_operation_ids(app)

Совет

Если вы вызываете app.openapi() вручную, обновите operationId до этого.

Предупреждение

Если вы делаете это, убедитесь, что каждая из ваших функций-обработчиков пути имеет уникальное имя.

Даже если они находятся в разных модулях (файлах Python).

Исключить из OpenAPI

Чтобы исключить операцию пути из генерируемой схемы OpenAPI (а значит, и из автоматической документации), используйте параметр include_in_schema и установите его в False:

from fastapi import FastAPI

app = FastAPI()


@app.get("/items/", include_in_schema=False)
async def read_items():
    return [{"item_id": "Foo"}]

Расширенное описание из docstring

Вы можете ограничить количество строк из docstring функции-обработчика пути, используемых для OpenAPI.

Добавление \f (экранированного символа «form feed») заставит FastAPI обрезать текст, используемый для OpenAPI, в этой точке.

Эта часть не попадёт в документацию, но другие инструменты (например, Sphinx) смогут использовать остальное.

from typing import Set, Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: Set[str] = set()


@app.post("/items/", response_model=Item, summary="Create an item")
async def create_item(item: Item):
    """
    Create an item with all the information:

    - **name**: each item must have a name
    - **description**: a long description
    - **price**: required
    - **tax**: if the item doesn't have tax, you can omit this
    - **tags**: a set of unique tag strings for this item
    \f
    :param item: User input.
    """
    return item

Дополнительные ответы

Вы, вероятно, уже видели, как объявлять response_model и status_code для операции пути.

Это определяет метаданные об основном ответе операции пути.

Также можно объявлять дополнительные ответы с их моделями, статус-кодами и т.д.

В документации есть целая глава об этом — Дополнительные ответы в OpenAPI.

Дополнительные данные OpenAPI

Когда вы объявляете операцию пути в своём приложении, FastAPI автоматически генерирует соответствующие метаданные об этой операции пути для включения в схему OpenAPI.

Технические детали

В спецификации OpenAPI это называется Объект операции.

Он содержит всю информацию об операции пути и используется для генерации автоматической документации.

Там есть tags, parameters, requestBody, responses и т.д.

Эта спецификация OpenAPI, специфичная для операции пути, обычно генерируется автоматически FastAPI, но вы также можете её расширить.

Совет

Это низкоуровневая возможность расширения.

Если вам нужно лишь объявить дополнительные ответы, удобнее сделать это через Дополнительные ответы в OpenAPI.

Вы можете расширить схему OpenAPI для операции пути с помощью параметра openapi_extra.

Расширения OpenAPI

openapi_extra может пригодиться, например, чтобы объявить Расширения OpenAPI:

from fastapi import FastAPI

app = FastAPI()


@app.get("/items/", openapi_extra={"x-aperture-labs-portal": "blue"})
async def read_items():
    return [{"item_id": "portal-gun"}]

Если вы откроете автоматическую документацию API, ваше расширение появится внизу страницы конкретной операции пути.

И если вы посмотрите на итоговый OpenAPI (по адресу /openapi.json вашего API), вы также увидите своё расширение в составе описания соответствующей операции пути:

{
    "openapi": "3.1.0",
    "info": {
        "title": "FastAPI",
        "version": "0.1.0"
    },
    "paths": {
        "/items/": {
            "get": {
                "summary": "Read Items",
                "operationId": "read_items_items__get",
                "responses": {
                    "200": {
                        "description": "Successful Response",
                        "content": {
                            "application/json": {
                                "schema": {}
                            }
                        }
                    }
                },
                "x-aperture-labs-portal": "blue"
            }
        }
    }
}

Пользовательская схема OpenAPI для операции пути

Словарь в openapi_extra будет объединён с автоматически сгенерированной схемой OpenAPI для операции пути.

Таким образом, вы можете добавить дополнительные данные к автоматически сгенерированной схеме.

Например, вы можете решить читать и валидировать запрос своим кодом, не используя автоматические возможности FastAPI и Pydantic, но при этом захотите описать запрос в схеме OpenAPI.

Это можно сделать с помощью openapi_extra:

from fastapi import FastAPI, Request

app = FastAPI()


def magic_data_reader(raw_body: bytes):
    return {
        "size": len(raw_body),
        "content": {
            "name": "Maaaagic",
            "price": 42,
            "description": "Just kiddin', no magic here. ✨",
        },
    }


@app.post(
    "/items/",
    openapi_extra={
        "requestBody": {
            "content": {
                "application/json": {
                    "schema": {
                        "required": ["name", "price"],
                        "type": "object",
                        "properties": {
                            "name": {"type": "string"},
                            "price": {"type": "number"},
                            "description": {"type": "string"},
                        },
                    }
                }
            },
            "required": True,
        },
    },
)
async def create_item(request: Request):
    raw_body = await request.body()
    data = magic_data_reader(raw_body)
    return data

В этом примере мы не объявляли никакую Pydantic-модель. Фактически тело запроса даже не распарсено как JSON, оно читается напрямую как bytes, а функция magic_data_reader() будет отвечать за его парсинг каким-то способом.

Тем не менее, мы можем объявить ожидаемую схему для тела запроса.

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

Используя тот же приём, вы можете воспользоваться Pydantic-моделью, чтобы определить JSON Schema, которая затем будет включена в пользовательский раздел схемы OpenAPI для операции пути.

И вы можете сделать это, даже если тип данных в запросе — не JSON.

Например, в этом приложении мы не используем встроенную функциональность FastAPI для извлечения JSON Schema из моделей Pydantic, равно как и автоматическую валидацию JSON. Мы объявляем тип содержимого запроса как YAML, а не JSON:

from typing import List

import yaml
from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel, ValidationError

app = FastAPI()


class Item(BaseModel):
    name: str
    tags: List[str]


@app.post(
    "/items/",
    openapi_extra={
        "requestBody": {
            "content": {"application/x-yaml": {"schema": Item.model_json_schema()}},
            "required": True,
        },
    },
)
async def create_item(request: Request):
    raw_body = await request.body()
    try:
        data = yaml.safe_load(raw_body)
    except yaml.YAMLError:
        raise HTTPException(status_code=422, detail="Invalid YAML")
    try:
        item = Item.model_validate(data)
    except ValidationError as e:
        raise HTTPException(status_code=422, detail=e.errors(include_url=False))
    return item
from typing import List

import yaml
from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel, ValidationError

app = FastAPI()


class Item(BaseModel):
    name: str
    tags: List[str]


@app.post(
    "/items/",
    openapi_extra={
        "requestBody": {
            "content": {"application/x-yaml": {"schema": Item.schema()}},
            "required": True,
        },
    },
)
async def create_item(request: Request):
    raw_body = await request.body()
    try:
        data = yaml.safe_load(raw_body)
    except yaml.YAMLError:
        raise HTTPException(status_code=422, detail="Invalid YAML")
    try:
        item = Item.parse_obj(data)
    except ValidationError as e:
        raise HTTPException(status_code=422, detail=e.errors())
    return item

Информация

В Pydantic версии 1 метод для получения JSON Schema модели назывался Item.schema(), в Pydantic версии 2 метод называется Item.model_json_schema().

Тем не менее, хотя мы не используем встроенную функциональность по умолчанию, мы всё равно используем Pydantic-модель, чтобы вручную сгенерировать JSON Schema для данных, которые мы хотим получить в YAML.

Затем мы работаем с запросом напрямую и извлекаем тело как bytes. Это означает, что FastAPI даже не попытается распарсить полезную нагрузку запроса как JSON.

А затем в нашем коде мы напрямую парсим этот YAML и снова используем ту же Pydantic-модель для валидации YAML-содержимого:

from typing import List

import yaml
from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel, ValidationError

app = FastAPI()


class Item(BaseModel):
    name: str
    tags: List[str]


@app.post(
    "/items/",
    openapi_extra={
        "requestBody": {
            "content": {"application/x-yaml": {"schema": Item.model_json_schema()}},
            "required": True,
        },
    },
)
async def create_item(request: Request):
    raw_body = await request.body()
    try:
        data = yaml.safe_load(raw_body)
    except yaml.YAMLError:
        raise HTTPException(status_code=422, detail="Invalid YAML")
    try:
        item = Item.model_validate(data)
    except ValidationError as e:
        raise HTTPException(status_code=422, detail=e.errors(include_url=False))
    return item
from typing import List

import yaml
from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel, ValidationError

app = FastAPI()


class Item(BaseModel):
    name: str
    tags: List[str]


@app.post(
    "/items/",
    openapi_extra={
        "requestBody": {
            "content": {"application/x-yaml": {"schema": Item.schema()}},
            "required": True,
        },
    },
)
async def create_item(request: Request):
    raw_body = await request.body()
    try:
        data = yaml.safe_load(raw_body)
    except yaml.YAMLError:
        raise HTTPException(status_code=422, detail="Invalid YAML")
    try:
        item = Item.parse_obj(data)
    except ValidationError as e:
        raise HTTPException(status_code=422, detail=e.errors())
    return item

Информация

В Pydantic версии 1 метод для парсинга и валидации объекта назывался Item.parse_obj(), в Pydantic версии 2 метод называется Item.model_validate().

Совет

Здесь мы переиспользуем ту же Pydantic-модель.

Но аналогично мы могли бы валидировать данные и каким-то другим способом.