Body - Modelos Anidados¶
Con FastAPI, puedes definir, validar, documentar y usar modelos arbitrariamente profundos (gracias a Pydantic).
Campos de tipo lista¶
Puedes definir un atributo para que sea un subtipo. Por ejemplo, una list de Python:
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 = []
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
results = {"item_id": item_id, "item": item}
return results
Esto hará que tags sea una lista, aunque no declara el tipo de los elementos de la lista.
Campos de tipo lista con parámetro de tipo¶
Pero Python tiene una manera específica de declarar listas con tipos internos, o "parámetros de tipo":
Declarar una list con un parámetro de tipo¶
Para declarar tipos que tienen parámetros de tipo (tipos internos), como list, dict, tuple,
pasa el(los) tipo(s) interno(s) como "parámetros de tipo" usando corchetes: [ y ]
my_list: list[str]
Esa es toda la sintaxis estándar de Python para declaraciones de tipos.
Usa esa misma sintaxis estándar para atributos de modelos con tipos internos.
Así, en nuestro ejemplo, podemos hacer que tags sea específicamente una "lista de strings":
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.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
results = {"item_id": item_id, "item": item}
return results
Tipos set¶
Pero luego lo pensamos, y nos damos cuenta de que los tags no deberían repetirse, probablemente serían strings únicos.
Y Python tiene un tipo de dato especial para conjuntos de elementos únicos, el set.
Entonces podemos declarar tags como un set de strings:
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.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
results = {"item_id": item_id, "item": item}
return results
Con esto, incluso si recibes una petición con datos duplicados, se convertirá a un conjunto de elementos únicos.
Y siempre que devuelvas esos datos, incluso si la fuente tenía duplicados, se devolverán como un conjunto de elementos únicos.
Y también será anotado / documentado en consecuencia.
Modelos Anidados¶
Cada atributo de un modelo Pydantic tiene un tipo.
Pero ese tipo puede ser a su vez otro modelo Pydantic.
Así, puedes declarar "objetos" JSON profundamente anidados con nombres de atributos, tipos y validaciones específicos.
Todo eso, anidado arbitrariamente.
Definir un submodelo¶
Por ejemplo, podemos definir un modelo Image:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Image(BaseModel):
url: str
name: str
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
tags: set[str] = set()
image: Image | None = None
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
results = {"item_id": item_id, "item": item}
return results
Usar el submodelo como tipo¶
Y luego podemos usarlo como el tipo de un atributo:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Image(BaseModel):
url: str
name: str
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
tags: set[str] = set()
image: Image | None = None
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
results = {"item_id": item_id, "item": item}
return results
Esto significaría que FastAPI esperaría un body similar a:
{
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2,
"tags": ["rock", "metal", "bar"],
"image": {
"url": "http://example.com/baz.jpg",
"name": "The Foo live"
}
}
De nuevo, haciendo solo esa declaración, con FastAPI obtienes:
- Soporte del editor (autocompletado, etc.), incluso para modelos anidados
- Conversión de datos
- Validación de datos
- Documentación automática
Tipos especiales y validación¶
Además de los tipos singulares normales como str, int, float, etc., puedes usar tipos singulares más complejos que heredan de str.
Para ver todas las opciones que tienes, consulta la descripción de tipos de Pydantic. Verás algunos ejemplos en el próximo capítulo.
Por ejemplo, como en el modelo Image tenemos un campo url, podemos declararlo como una instancia de HttpUrl de Pydantic en lugar de un str:
from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl
app = FastAPI()
class Image(BaseModel):
url: HttpUrl
name: str
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
tags: set[str] = set()
image: Image | None = None
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
results = {"item_id": item_id, "item": item}
return results
El string será verificado para asegurar que es una URL válida, y documentado en JSON Schema / OpenAPI como tal.
Atributos con listas de submodelos¶
También puedes usar modelos Pydantic como subtipos de list, set, etc.:
from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl
app = FastAPI()
class Image(BaseModel):
url: HttpUrl
name: str
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
tags: set[str] = set()
images: list[Image] | None = None
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
results = {"item_id": item_id, "item": item}
return results
Esto esperará (convertirá, validará, documentará, etc.) un body JSON como:
{
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2,
"tags": [
"rock",
"metal",
"bar"
],
"images": [
{
"url": "http://example.com/baz.jpg",
"name": "The Foo live"
},
{
"url": "http://example.com/dave.jpg",
"name": "The Baz"
}
]
}
Nota
Nota cómo la clave images ahora tiene una lista de objetos imagen.
Modelos profundamente anidados¶
Puedes definir modelos arbitrariamente profundos:
from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl
app = FastAPI()
class Image(BaseModel):
url: HttpUrl
name: str
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
tags: set[str] = set()
images: list[Image] | None = None
class Offer(BaseModel):
name: str
description: str | None = None
price: float
items: list[Item]
@app.post("/offers/")
async def create_offer(offer: Offer):
return offer
Nota
Nota cómo Offer tiene una lista de Items, que a su vez tienen una lista opcional de Images
Bodies de listas puras¶
Si el valor de nivel superior del body JSON que esperas es un array JSON (una list de Python), puedes declarar el tipo en el parámetro de la función, igual que en los modelos Pydantic:
images: list[Image]
como en:
from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl
app = FastAPI()
class Image(BaseModel):
url: HttpUrl
name: str
@app.post("/images/multiple/")
async def create_multiple_images(images: list[Image]):
return images
Soporte del editor en todas partes¶
Y obtienes soporte del editor en todas partes.
Incluso para elementos dentro de listas:

No podrías obtener este tipo de soporte del editor si estuvieras trabajando directamente con dict en lugar de modelos Pydantic.
Pero tampoco tienes que preocuparte por ellos, los dicts entrantes se convierten automáticamente y tu salida se convierte automáticamente a JSON también.
Bodies de dicts arbitrarios¶
También puedes declarar un body como un dict con claves de un tipo y valores de otro tipo.
De esta manera, no tienes que saber de antemano cuáles son los nombres válidos de campos/atributos (como sería el caso con modelos Pydantic).
Esto sería útil si quieres recibir claves que ya no conoces.
Otro caso útil es cuando quieres tener claves de otro tipo (por ejemplo, int).
Eso es lo que vamos a ver aquí.
En este caso, aceptarías cualquier dict siempre que tenga claves int con valores float:
from fastapi import FastAPI
app = FastAPI()
@app.post("/index-weights/")
async def create_index_weights(weights: dict[int, float]):
return weights
Consejo
Ten en cuenta que JSON solo soporta str como claves.
Pero Pydantic tiene conversión automática de datos.
Esto significa que, aunque tus clientes API solo pueden enviar strings como claves, siempre que esos strings contengan enteros puros, Pydantic los convertirá y validará.
Y el dict que recibes como weights tendrá realmente claves int y valores float.
Resumen¶
Con FastAPI tienes la máxima flexibilidad proporcionada por los modelos Pydantic, manteniendo tu código simple, corto y elegante.
Pero con todos los beneficios:
- Soporte del editor (¡autocompletado en todas partes!)
- Conversión de datos (también conocido como parsing / serialización)
- Validación de datos
- Documentación del esquema
- Documentación automática