Saltar al contenido

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