Saltar al contenido

Modelo de Respuesta - Tipo de Retorno

Puedes declarar el tipo usado para la respuesta anotando el tipo de retorno de la función de path operation.

Puedes usar anotaciones de tipo de la misma manera que lo harías para los datos de entrada en los parámetros de la función, puedes usar modelos de Pydantic, listas, diccionarios, valores escalares como enteros, booleanos, etc.

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: list[str] = []


@app.post("/items/")
async def create_item(item: Item) -> Item:
    return item


@app.get("/items/")
async def read_items() -> list[Item]:
    return [
        Item(name="Portal Gun", price=42.0),
        Item(name="Plumbus", price=32.0),
    ]

FastAPI usará este tipo de retorno para:

  • Validate the returned data.
    • Si los datos son inválidos (por ejemplo, falta un campo), significa que el código de tu aplicación está roto, no está devolviendo lo que debería, y devolverá un error de servidor en lugar de devolver datos incorrectos. De esta manera, tú y tus clientes pueden estar seguros de que recibirán los datos y la estructura de datos esperada.
  • Add a JSON Schema for the response, in the OpenAPI path operation.
    • Esto será usado por la documentación automática.
    • También será usado por herramientas automáticas de generación de código cliente.
  • Serializar los datos devueltos a JSON usando Pydantic, que está escrito en Rust, por lo que será mucho más rápido.

Pero lo más importante:

  • It will limit and filter the output data to what is defined in the return type.
    • Esto es particularmente importante para la seguridad, veremos más de eso a continuación.

Parámetro response_model

Hay algunos casos donde necesitas o quieres devolver algunos datos que no son exactamente lo que el tipo declara.

Por ejemplo, podrías querer devolver un diccionario o un objeto de base de datos, pero declararlo como un modelo de Pydantic. De esta manera, el modelo de Pydantic haría toda la documentación de datos, validación, etc. para el objeto que devolviste (por ejemplo, un diccionario u objeto de base de datos).

Si agregaras la anotación de tipo de retorno, las herramientas y editores se quejarían con un error (correcto) diciéndote que tu función está devolviendo un tipo (por ejemplo, un dict) que es diferente de lo que declaraste (por ejemplo, un modelo de Pydantic).

En esos casos, puedes usar el parámetro response_model del decorador de path operation en lugar del tipo de retorno.

Puedes usar el parámetro response_model en cualquiera de las path operations:

  • @app.get()
  • @app.post()
  • @app.put()
  • @app.delete()
  • etc.
from typing import Any

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: list[str] = []


@app.post("/items/", response_model=Item)
async def create_item(item: Item) -> Any:
    return item


@app.get("/items/", response_model=list[Item])
async def read_items() -> Any:
    return [
        {"name": "Portal Gun", "price": 42.0},
        {"name": "Plumbus", "price": 32.0},
    ]

Nota

Ten en cuenta que response_model es un parámetro del método "decorador" (get, post, etc). No de tu función de path operation, como todos los parámetros y el body.

response_model recibe el mismo tipo que declararías para un campo de un modelo de Pydantic, por lo que puede ser un modelo de Pydantic, pero también puede ser, por ejemplo, una list de modelos de Pydantic, como List[Item].

FastAPI usará este response_model para hacer toda la documentación de datos, validación, etc. y también para convertir y filtrar los datos de salida a su declaración de tipo.

Consejo

Si tienes verificaciones de tipo estrictas en tu editor, mypy, etc, puedes declarar el tipo de retorno de la función como Any.

De esa manera le dices al editor que estás devolviendo intencionalmente cualquier cosa. Pero FastAPI seguirá haciendo la documentación de datos, validación, filtrado, etc. con el response_model.

Prioridad de response_model

Si declaras tanto un tipo de retorno como un response_model, el response_model tomará prioridad y será usado por FastAPI.

De esta manera puedes agregar anotaciones de tipo correctas a tus funciones incluso cuando estás devolviendo un tipo diferente al modelo de respuesta, para ser usadas por el editor y herramientas como mypy. Y aún así puedes hacer que FastAPI haga la validación de datos, documentación, etc. usando el response_model.

También puedes usar response_model=None para deshabilitar la creación de un modelo de respuesta para esa path operation, podrías necesitar hacerlo si estás agregando anotaciones de tipo para cosas que no son campos válidos de Pydantic, verás un ejemplo de eso en una de las secciones a continuación.

Devolver los mismos datos de entrada

Aquí estamos declarando un modelo UserIn, contendrá una contraseña en texto plano:

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: str | None = None


# Don't do this in production!
@app.post("/user/")
async def create_user(user: UserIn) -> UserIn:
    return user

Nota

Para usar EmailStr, primero instala email-validator.

Asegúrate de crear un entorno virtual, activarlo, y luego instalarlo, por ejemplo:

$ pip install email-validator

o con:

$ pip install "pydantic[email]"

Y estamos usando este modelo para declarar nuestra entrada y el mismo modelo para declarar nuestra salida:

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: str | None = None


# Don't do this in production!
@app.post("/user/")
async def create_user(user: UserIn) -> UserIn:
    return user

Ahora, cada vez que un navegador cree un usuario con una contraseña, la API devolverá la misma contraseña en la respuesta.

En este caso, podría no ser un problema, porque es el mismo usuario enviando la contraseña.

Pero si usamos el mismo modelo para otra path operation, podríamos estar enviando las contraseñas de nuestros usuarios a cada cliente.

Peligro

Nunca almacenes la contraseña en texto plano de un usuario ni la envíes en una respuesta como esta, a menos que conozcas todas las advertencias y sepas lo que estás haciendo.

Agregar un modelo de salida

En su lugar, podemos crear un modelo de entrada con la contraseña en texto plano y un modelo de salida sin ella:

from typing import Any

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: str | None = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn) -> Any:
    return user

Aquí, aunque nuestra función de path operation está devolviendo el mismo usuario de entrada que contiene la contraseña:

from typing import Any

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: str | None = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn) -> Any:
    return user

...declaramos el response_model para que sea nuestro modelo UserOut, que no incluye la contraseña:

from typing import Any

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: str | None = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn) -> Any:
    return user

Así, FastAPI se encargará de filtrar todos los datos que no están declarados en el modelo de salida (usando Pydantic).

response_model o Tipo de Retorno

En este caso, como los dos modelos son diferentes, si anotáramos el tipo de retorno de la función como UserOut, el editor y las herramientas se quejarían de que estamos devolviendo un tipo inválido, ya que son clases diferentes.

Es por eso que en este ejemplo tenemos que declararlo en el parámetro response_model.

...pero continúa leyendo a continuación para ver cómo superar eso.

Tipo de Retorno y Filtrado de Datos

Continuemos desde el ejemplo anterior. Queríamos anotar la función con un tipo, pero queríamos poder devolver desde la función algo que en realidad incluye más datos.

Queremos que FastAPI siga filtrando los datos usando el modelo de respuesta. De manera que aunque la función devuelva más datos, la respuesta solo incluya los campos declarados en el modelo de respuesta.

En el ejemplo anterior, como las clases eran diferentes, tuvimos que usar el parámetro response_model. Pero eso también significa que no obtenemos el soporte del editor y las herramientas para verificar el tipo de retorno de la función.

Pero en la mayoría de los casos donde necesitamos hacer algo como esto, queremos que el modelo simplemente filtre/elimine algunos de los datos como en este ejemplo.

Y en esos casos, podemos usar clases y herencia para aprovechar las anotaciones de tipo de la función para obtener mejor soporte en el editor y las herramientas, y aún así obtener el filtrado de datos de FastAPI.

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class BaseUser(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


class UserIn(BaseUser):
    password: str


@app.post("/user/")
async def create_user(user: UserIn) -> BaseUser:
    return user

Con esto, obtenemos soporte de herramientas, de editores y mypy ya que este código es correcto en términos de tipos, pero también obtenemos el filtrado de datos de FastAPI.

¿Cómo funciona esto? Vamos a comprobarlo. 🤓

Anotaciones de Tipo y Herramientas

Primero veamos cómo los editores, mypy y otras herramientas verían esto.

BaseUser tiene los campos base. Luego UserIn hereda de BaseUser y agrega el campo password, por lo que incluirá todos los campos de ambos modelos.

Anotamos el tipo de retorno de la función como BaseUser, pero en realidad estamos devolviendo una instancia de UserIn.

El editor, mypy y otras herramientas no se quejarán de esto porque, en términos de tipado, UserIn es una subclase de BaseUser, lo que significa que es un tipo válido cuando lo que se espera es cualquier cosa que sea un BaseUser.

Filtrado de Datos de FastAPI

Ahora, para FastAPI, verá el tipo de retorno y se asegurará de que lo que devuelvas incluya solo los campos que están declarados en el tipo.

FastAPI hace varias cosas internamente con Pydantic para asegurar que esas mismas reglas de herencia de clases no se usen para el filtrado de los datos devueltos, de lo contrario podrías terminar devolviendo muchos más datos de los que esperabas.

De esta manera, puedes obtener lo mejor de ambos mundos: anotaciones de tipo con soporte de herramientas y filtrado de datos.

Verlo en la documentación

Cuando veas la documentación automática, puedes comprobar que el modelo de entrada y el modelo de salida tendrán ambos su propio JSON Schema:

Y ambos modelos serán usados para la documentación interactiva de la API:

Otras Anotaciones de Tipo de Retorno

Puede haber casos donde devuelves algo que no es un campo válido de Pydantic y lo anotas en la función, solo para obtener el soporte proporcionado por las herramientas (el editor, mypy, etc).

Devolver una Response Directamente

El caso más común sería devolver una Response directamente como se explica más adelante en la documentación avanzada.

from fastapi import FastAPI, Response
from fastapi.responses import JSONResponse, RedirectResponse

app = FastAPI()


@app.get("/portal")
async def get_portal(teleport: bool = False) -> Response:
    if teleport:
        return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
    return JSONResponse(content={"message": "Here's your interdimensional portal."})

Este caso simple es manejado automáticamente por FastAPI porque la anotación de tipo de retorno es la clase (o una subclase de) Response.

Y las herramientas también estarán contentas porque tanto RedirectResponse como JSONResponse son subclases de Response, por lo que la anotación de tipo es correcta.

Anotar una Subclase de Response

También puedes usar una subclase de Response en la anotación de tipo:

from fastapi import FastAPI
from fastapi.responses import RedirectResponse

app = FastAPI()


@app.get("/teleport")
async def get_teleport() -> RedirectResponse:
    return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")

Esto también funcionará porque RedirectResponse es una subclase de Response, y FastAPI manejará automáticamente este caso simple.

Anotaciones de Tipo de Retorno Inválidas

Pero cuando devuelves algún otro objeto arbitrario que no es un tipo válido de Pydantic (por ejemplo, un objeto de base de datos) y lo anotas así en la función, FastAPI intentará crear un modelo de respuesta de Pydantic a partir de esa anotación de tipo, y fallará.

Lo mismo sucedería si tuvieras algo como una unión entre diferentes tipos donde uno o más de ellos no son tipos válidos de Pydantic, por ejemplo esto fallaría 💥:

from fastapi import FastAPI, Response
from fastapi.responses import RedirectResponse

app = FastAPI()


@app.get("/portal")
async def get_portal(teleport: bool = False) -> Response | dict:
    if teleport:
        return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
    return {"message": "Here's your interdimensional portal."}

...esto falla porque la anotación de tipo no es un tipo de Pydantic y no es solo una clase Response o subclase, es una unión (cualquiera de los dos) entre una Response y un dict.

Deshabilitar el Modelo de Respuesta

Continuando con el ejemplo anterior, podrías no querer tener la validación de datos por defecto, documentación, filtrado, etc. que realiza FastAPI.

Pero podrías querer mantener la anotación de tipo de retorno en la función para obtener el soporte de herramientas como editores y verificadores de tipo (por ejemplo, mypy).

En este caso, puedes deshabilitar la generación del modelo de respuesta estableciendo response_model=None:

from fastapi import FastAPI, Response
from fastapi.responses import RedirectResponse

app = FastAPI()


@app.get("/portal", response_model=None)
async def get_portal(teleport: bool = False) -> Response | dict:
    if teleport:
        return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
    return {"message": "Here's your interdimensional portal."}

Esto hará que FastAPI omita la generación del modelo de respuesta y de esa manera puedes tener cualquier anotación de tipo de retorno que necesites sin que afecte tu aplicación FastAPI. 🤓

Parámetros de codificación del Modelo de Respuesta

Tu modelo de respuesta podría tener valores por defecto, como:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


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


items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
    "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}


@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True)
async def read_item(item_id: str):
    return items[item_id]
  • description: Union[str, None] = None (o str | None = None en Python 3.10) tiene un valor por defecto de None.
  • tax: float = 10.5 tiene un valor por defecto de 10.5.
  • tags: List[str] = [] tiene como valor por defecto una lista vacía: [].

pero podrías querer omitirlos del resultado si no fueron realmente almacenados.

Por ejemplo, si tienes modelos con muchos atributos opcionales en una base de datos NoSQL, pero no quieres enviar respuestas JSON muy largas llenas de valores por defecto.

Usar el parámetro response_model_exclude_unset

Puedes establecer el parámetro response_model_exclude_unset=True del decorador de path operation:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


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


items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
    "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}


@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True)
async def read_item(item_id: str):
    return items[item_id]

y esos valores por defecto no serán incluidos en la respuesta, solo los valores realmente establecidos.

Así, si envías una petición a esa path operation para el item con ID foo, la respuesta (sin incluir los valores por defecto) será:

{
    "name": "Foo",
    "price": 50.2
}

Nota

También puedes usar:

  • response_model_exclude_defaults=True
  • response_model_exclude_none=True

como se describe en la documentación de Pydantic para exclude_defaults y exclude_none.

Datos con valores para campos con valores por defecto

Pero si tus datos tienen valores para los campos del modelo con valores por defecto, como el item con ID bar:

{
    "name": "Bar",
    "description": "The bartenders",
    "price": 62,
    "tax": 20.2
}

serán incluidos en la respuesta.

Datos con los mismos valores que los por defecto

Si los datos tienen los mismos valores que los por defecto, como el item con ID baz:

{
    "name": "Baz",
    "description": None,
    "price": 50.2,
    "tax": 10.5,
    "tags": []
}

FastAPI es lo suficientemente inteligente (en realidad, Pydantic es lo suficientemente inteligente) para darse cuenta de que, aunque description, tax y tags tienen los mismos valores que los por defecto, fueron establecidos explícitamente (en lugar de tomarse de los valores por defecto).

Así, serán incluidos en la respuesta JSON.

Consejo

Ten en cuenta que los valores por defecto pueden ser cualquier cosa, no solo None.

Pueden ser una lista ([]), un float de 10.5, etc.

response_model_include y response_model_exclude

También puedes usar los parámetros response_model_include y response_model_exclude del decorador de path operation.

Toman un set de str con el nombre de los atributos a incluir (omitiendo el resto) o a excluir (incluyendo el resto).

Esto se puede usar como un atajo rápido si tienes solo un modelo de Pydantic y quieres eliminar algunos datos de la salida.

Consejo

Pero aún se recomienda usar las ideas anteriores, usando múltiples clases, en lugar de estos parámetros.

Esto se debe a que el JSON Schema generado en el OpenAPI de tu aplicación (y la documentación) seguirá siendo el del modelo completo, incluso si usas response_model_include o response_model_exclude para omitir algunos atributos.

Esto también aplica a response_model_by_alias que funciona de manera similar.

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float = 10.5


items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2},
    "baz": {
        "name": "Baz",
        "description": "There goes my baz",
        "price": 50.2,
        "tax": 10.5,
    },
}


@app.get(
    "/items/{item_id}/name",
    response_model=Item,
    response_model_include={"name", "description"},
)
async def read_item_name(item_id: str):
    return items[item_id]


@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude={"tax"})
async def read_item_public_data(item_id: str):
    return items[item_id]

Consejo

La sintaxis {"name", "description"} crea un set con esos dos valores.

Es equivalente a set(["name", "description"]).

Usando lists en lugar de sets

Si olvidas usar un set y usas una list o tuple en su lugar, FastAPI aún lo convertirá a un set y funcionará correctamente:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float = 10.5


items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2},
    "baz": {
        "name": "Baz",
        "description": "There goes my baz",
        "price": 50.2,
        "tax": 10.5,
    },
}


@app.get(
    "/items/{item_id}/name",
    response_model=Item,
    response_model_include=["name", "description"],
)
async def read_item_name(item_id: str):
    return items[item_id]


@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude=["tax"])
async def read_item_public_data(item_id: str):
    return items[item_id]

Resumen

Usa el parámetro response_model del decorador de path operation para definir modelos de respuesta y especialmente para asegurar que los datos privados sean filtrados.

Usa response_model_exclude_unset para devolver solo los valores establecidos explícitamente.