Saltar al contenido

Configuración Avanzada de Operaciones de Ruta

operationId de OpenAPI

Aviso

Si no eres un "experto" en OpenAPI, probablemente no necesites esto.

Puedes establecer el operationId de OpenAPI a usar en tu operación de ruta con el parámetro operation_id.

Tendrías que asegurarte de que sea único para cada operación.

from fastapi import FastAPI

app = FastAPI()


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

Usar el nombre de la función de operación de ruta como operationId

Si quieres usar los nombres de las funciones de tus APIs como operationIds, puedes pasar una generate_unique_id_function personalizada a FastAPI.

La función recibe cada APIRoute y devuelve el operationId a usar para esa operación de ruta.

from fastapi import FastAPI
from fastapi.routing import APIRoute


def custom_generate_unique_id(route: APIRoute) -> str:
    return route.name


app = FastAPI(generate_unique_id_function=custom_generate_unique_id)


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

Aviso

Si haces esto, tienes que asegurarte de que cada una de tus funciones de operación de ruta tenga un nombre único.

Incluso si están en diferentes módulos (archivos de Python).

Excluir de OpenAPI

Para excluir una operación de ruta del esquema OpenAPI generado (y por lo tanto, de los sistemas de documentación automática), usa el parámetro include_in_schema y configúralo como False:

from fastapi import FastAPI

app = FastAPI()


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

Descripción avanzada desde el docstring

Puedes limitar las líneas usadas del docstring de una función de operación de ruta para OpenAPI.

Añadir un \f (un carácter escapado de "form feed") hace que FastAPI trunque la salida usada para OpenAPI en este punto.

No aparecerá en la documentación, pero otras herramientas (como Sphinx) podrán usar el resto.

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


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


@app.post("/items/", summary="Create an item")
async def create_item(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

Respuestas Adicionales

Probablemente ya has visto cómo declarar el response_model y el status_code para una operación de ruta.

Eso define los metadatos sobre la respuesta principal de una operación de ruta.

También puedes declarar respuestas adicionales con sus modelos, códigos de estado, etc.

Hay un capítulo completo aquí en la documentación sobre esto, puedes leerlo en Respuestas Adicionales en OpenAPI.

OpenAPI Extra

Cuando declaras una operación de ruta en tu aplicación, FastAPI genera automáticamente los metadatos relevantes sobre esa operación de ruta para incluirlos en el esquema de OpenAPI.

Detalles técnicos

En la especificación de OpenAPI se llama Operation Object.

Tiene toda la información sobre la operación de ruta y se usa para generar la documentación automática.

Incluye las etiquetas, parámetros, requestBody, responses, etc.

Este esquema de OpenAPI específico de la operación de ruta normalmente es generado automáticamente por FastAPI, pero también puedes extenderlo.

Consejo

Este es un punto de extensión de bajo nivel.

Si solo necesitas declarar respuestas adicionales, una forma más conveniente de hacerlo es con Respuestas Adicionales en OpenAPI.

Puedes extender el esquema de OpenAPI para una operación de ruta usando el parámetro openapi_extra.

Extensiones de OpenAPI

Este openapi_extra puede ser útil, por ejemplo, para declarar Extensiones de 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"}]

Si abres la documentación automática de la API, tu extensión aparecerá al final de la operación de ruta específica.

Y si ves el OpenAPI resultante (en /openapi.json en tu API), verás tu extensión como parte de la operación de ruta específica también:

{
    "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"
            }
        }
    }
}

Esquema personalizado de operación de ruta en OpenAPI

El diccionario en openapi_extra se combinará profundamente con el esquema de OpenAPI generado automáticamente para la operación de ruta.

Así que, podrías añadir datos adicionales al esquema generado automáticamente.

Por ejemplo, podrías decidir leer y validar la petición con tu propio código, sin usar las características automáticas de FastAPI con Pydantic, pero aún así podrías querer definir la petición en el esquema de OpenAPI.

Podrías hacer eso con 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

En este ejemplo, no declaramos ningún modelo de Pydantic. De hecho, el cuerpo de la petición ni siquiera se analiza como JSON, se lee directamente como bytes, y la función magic_data_reader() se encargaría de analizarlo de alguna manera.

Sin embargo, podemos declarar el esquema esperado para el cuerpo de la petición.

Tipo de contenido personalizado en OpenAPI

Usando este mismo truco, podrías usar un modelo de Pydantic para definir el JSON Schema que luego se incluye en la sección personalizada del esquema de OpenAPI para la operación de ruta.

Y podrías hacer esto incluso si el tipo de dato de la petición no es JSON.

Por ejemplo, en esta aplicación no usamos la funcionalidad integrada de FastAPI para extraer el JSON Schema de los modelos de Pydantic ni la validación automática para JSON. De hecho, estamos declarando el tipo de contenido de la petición como YAML, no como JSON:

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

Sin embargo, aunque no estamos usando la funcionalidad integrada por defecto, seguimos usando un modelo de Pydantic para generar manualmente el JSON Schema para los datos que queremos recibir en YAML.

Luego usamos la petición directamente, y extraemos el cuerpo como bytes. Esto significa que FastAPI ni siquiera intentará analizar el payload de la petición como JSON.

Y luego en nuestro código, analizamos ese contenido YAML directamente, y luego estamos usando nuevamente el mismo modelo de Pydantic para validar el contenido YAML:

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

Consejo

Aquí reutilizamos el mismo modelo de Pydantic.

Pero de la misma manera, podríamos haberlo validado de alguna otra forma.