Saltar al contenido

Respuestas Adicionales en OpenAPI

Aviso

Este es un tema bastante avanzado.

Si estás empezando con FastAPI, podrías no necesitar esto.

Puedes declarar respuestas adicionales, con códigos de estado adicionales, tipos de medios, descripciones, etc.

Esas respuestas adicionales se incluirán en el esquema de OpenAPI, por lo que también aparecerán en la documentación de la API.

Pero para esas respuestas adicionales tienes que asegurarte de devolver una Response como JSONResponse directamente, con tu código de estado y contenido.

Respuesta Adicional con model

Puedes pasar a tus decoradores de path operation un parámetro responses.

Recibe un dict: las claves son códigos de estado para cada respuesta (como 200), y los valores son otros dicts con la información de cada uno.

Cada uno de esos dicts de respuesta puede tener una clave model, que contiene un modelo de Pydantic, al igual que response_model.

FastAPI tomará ese modelo, generará su JSON Schema y lo incluirá en el lugar correcto en OpenAPI.

Por ejemplo, para declarar otra respuesta con un código de estado 404 y un modelo de Pydantic Message, puedes escribir:

from fastapi import FastAPI
from fastapi.responses import JSONResponse
from pydantic import BaseModel


class Item(BaseModel):
    id: str
    value: str


class Message(BaseModel):
    message: str


app = FastAPI()


@app.get("/items/{item_id}", response_model=Item, responses={404: {"model": Message}})
async def read_item(item_id: str):
    if item_id == "foo":
        return {"id": "foo", "value": "there goes my hero"}
    return JSONResponse(status_code=404, content={"message": "Item not found"})

Nota

Ten en cuenta que tienes que devolver el JSONResponse directamente.

Nota

La clave model no es parte de OpenAPI.

FastAPI tomará el modelo de Pydantic de ahí, generará el JSON Schema, y lo colocará en el lugar correcto.

El lugar correcto es:

  • In the key content, that has as value another JSON object (dict) that contains:
    • A key with the media type, e.g. application/json, that contains as value another JSON object, that contains:
      • A key schema, that has as the value the JSON Schema from the model, here's the correct place.
        • FastAPI añade una referencia aquí a los JSON Schemas globales en otro lugar de tu OpenAPI en lugar de incluirlo directamente. De esta manera, otras aplicaciones y clientes pueden usar esos JSON Schemas directamente, proporcionar mejores herramientas de generación de código, etc.

Las respuestas generadas en OpenAPI para esta path operation serán:

{
    "responses": {
        "404": {
            "description": "Additional Response",
            "content": {
                "application/json": {
                    "schema": {
                        "$ref": "#/components/schemas/Message"
                    }
                }
            }
        },
        "200": {
            "description": "Successful Response",
            "content": {
                "application/json": {
                    "schema": {
                        "$ref": "#/components/schemas/Item"
                    }
                }
            }
        },
        "422": {
            "description": "Validation Error",
            "content": {
                "application/json": {
                    "schema": {
                        "$ref": "#/components/schemas/HTTPValidationError"
                    }
                }
            }
        }
    }
}

Los esquemas están referenciados a otro lugar dentro del esquema de OpenAPI:

{
    "components": {
        "schemas": {
            "Message": {
                "title": "Message",
                "required": [
                    "message"
                ],
                "type": "object",
                "properties": {
                    "message": {
                        "title": "Message",
                        "type": "string"
                    }
                }
            },
            "Item": {
                "title": "Item",
                "required": [
                    "id",
                    "value"
                ],
                "type": "object",
                "properties": {
                    "id": {
                        "title": "Id",
                        "type": "string"
                    },
                    "value": {
                        "title": "Value",
                        "type": "string"
                    }
                }
            },
            "ValidationError": {
                "title": "ValidationError",
                "required": [
                    "loc",
                    "msg",
                    "type"
                ],
                "type": "object",
                "properties": {
                    "loc": {
                        "title": "Location",
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "msg": {
                        "title": "Message",
                        "type": "string"
                    },
                    "type": {
                        "title": "Error Type",
                        "type": "string"
                    }
                }
            },
            "HTTPValidationError": {
                "title": "HTTPValidationError",
                "type": "object",
                "properties": {
                    "detail": {
                        "title": "Detail",
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/ValidationError"
                        }
                    }
                }
            }
        }
    }
}

Tipos de medios adicionales para la respuesta principal

Puedes usar este mismo parámetro responses para añadir diferentes tipos de medios para la misma respuesta principal.

Por ejemplo, puedes añadir un tipo de medio adicional de image/png, declarando que tu path operation puede devolver un objeto JSON (con tipo de medio application/json) o una imagen PNG:

from fastapi import FastAPI
from fastapi.responses import FileResponse
from pydantic import BaseModel


class Item(BaseModel):
    id: str
    value: str


app = FastAPI()


@app.get(
    "/items/{item_id}",
    response_model=Item,
    responses={
        200: {
            "content": {"image/png": {}},
            "description": "Return the JSON item or an image.",
        }
    },
)
async def read_item(item_id: str, img: bool | None = None):
    if img:
        return FileResponse("image.png", media_type="image/png")
    else:
        return {"id": "foo", "value": "there goes my hero"}

Nota

Ten en cuenta que tienes que devolver la imagen usando un FileResponse directamente.

Nota

A menos que especifiques un tipo de medio diferente explícitamente en tu parámetro responses, FastAPI asumirá que la respuesta tiene el mismo tipo de medio que la clase de respuesta principal (por defecto application/json).

Pero si has especificado una clase de respuesta personalizada con None como tipo de medio, FastAPI usará application/json para cualquier respuesta adicional que tenga un modelo asociado.

Combinando información

También puedes combinar información de respuesta de múltiples lugares, incluyendo los parámetros response_model, status_code, y responses.

Puedes declarar un response_model, usando el código de estado por defecto 200 (o uno personalizado si lo necesitas), y luego declarar información adicional para esa misma respuesta en responses, directamente en el esquema de OpenAPI.

FastAPI mantendrá la información adicional de responses, y la combinará con el JSON Schema de tu modelo.

Por ejemplo, puedes declarar una respuesta con un código de estado 404 que usa un modelo de Pydantic y tiene una description personalizada.

Y una respuesta con un código de estado 200 que usa tu response_model, pero incluye un example personalizado:

from fastapi import FastAPI
from fastapi.responses import JSONResponse
from pydantic import BaseModel


class Item(BaseModel):
    id: str
    value: str


class Message(BaseModel):
    message: str


app = FastAPI()


@app.get(
    "/items/{item_id}",
    response_model=Item,
    responses={
        404: {"model": Message, "description": "The item was not found"},
        200: {
            "description": "Item requested by ID",
            "content": {
                "application/json": {
                    "example": {"id": "bar", "value": "The bar tenders"}
                }
            },
        },
    },
)
async def read_item(item_id: str):
    if item_id == "foo":
        return {"id": "foo", "value": "there goes my hero"}
    else:
        return JSONResponse(status_code=404, content={"message": "Item not found"})

Todo se combinará e incluirá en tu OpenAPI, y se mostrará en la documentación de la API:

Combinar respuestas predefinidas y personalizadas

Podrías querer tener algunas respuestas predefinidas que apliquen a muchas path operations, pero quieres combinarlas con respuestas personalizadas necesarias para cada path operation.

Para esos casos, puedes usar la técnica de Python de "desempaquetar" un dict con **dict_to_unpack:

old_dict = {
    "old key": "old value",
    "second old key": "second old value",
}
new_dict = {**old_dict, "new key": "new value"}

Aquí, new_dict contendrá todos los pares clave-valor de old_dict más el nuevo par clave-valor:

{
    "old key": "old value",
    "second old key": "second old value",
    "new key": "new value",
}

Puedes usar esa técnica para reutilizar algunas respuestas predefinidas en tus path operations y combinarlas con otras personalizadas adicionales.

Por ejemplo:

from fastapi import FastAPI
from fastapi.responses import FileResponse
from pydantic import BaseModel


class Item(BaseModel):
    id: str
    value: str


responses = {
    404: {"description": "Item not found"},
    302: {"description": "The item was moved"},
    403: {"description": "Not enough privileges"},
}


app = FastAPI()


@app.get(
    "/items/{item_id}",
    response_model=Item,
    responses={**responses, 200: {"content": {"image/png": {}}}},
)
async def read_item(item_id: str, img: bool | None = None):
    if img:
        return FileResponse("image.png", media_type="image/png")
    else:
        return {"id": "foo", "value": "there goes my hero"}

Más información sobre las respuestas de OpenAPI

Para ver qué puedes incluir exactamente en las respuestas, puedes revisar estas secciones en la especificación de OpenAPI:

  • OpenAPI Responses Object, incluye el Response Object.
  • OpenAPI Response Object, puedes incluir cualquier cosa de esto directamente en cada respuesta dentro de tu parámetro responses. Incluyendo description, headers, content (dentro de esto es donde declaras diferentes tipos de medios y JSON Schemas), y links.