Saltar a contenido

Separación de Esquemas OpenAPI para Entrada y Salida o No

Al usar Pydantic v2, el OpenAPI generado es un poco más exacto y correcto que antes. 😎

De hecho, en algunos casos, incluso tendrá dos JSON Schemas en OpenAPI para el mismo modelo Pydantic, para entrada y salida, dependiendo de si tienen valores por defecto.

Veamos cómo funciona eso y cómo cambiarlo si necesitas hacerlo.

Modelos Pydantic para Entrada y Salida

Digamos que tienes un modelo Pydantic con valores por defecto, como este:

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: str | None = None

# Code below omitted 👇
👀 Full file preview
from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: str | None = None


app = FastAPI()


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


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
🤓 Other versions and variants
from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Optional[str] = None


app = FastAPI()


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


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
from typing import List, Union

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Union[str, None] = None


app = FastAPI()


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


@app.get("/items/")
def read_items() -> List[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]

Modelo para Entrada

Si usas este modelo como entrada, como aquí:

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: str | None = None


app = FastAPI()


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

# Code below omitted 👇
👀 Full file preview
from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: str | None = None


app = FastAPI()


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


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
🤓 Other versions and variants
from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Optional[str] = None


app = FastAPI()


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


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
from typing import List, Union

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Union[str, None] = None


app = FastAPI()


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


@app.get("/items/")
def read_items() -> List[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]

...entonces el campo description no será requerido. Porque tiene un valor por defecto de None.

Modelo de Entrada en la Documentación

Puedes confirmar eso en la documentación, el campo description no tiene un asterisco rojo, no está marcado como requerido:

Modelo para Salida

Pero si usas el mismo modelo como salida, como aquí:

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: str | None = None


app = FastAPI()


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


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
🤓 Other versions and variants
from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Optional[str] = None


app = FastAPI()


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


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
from typing import List, Union

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Union[str, None] = None


app = FastAPI()


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


@app.get("/items/")
def read_items() -> List[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]

...entonces, porque description tiene un valor por defecto, si no devuelves nada para ese campo, aún tendrá ese valor por defecto.

Modelo para Datos de Response de Salida

Si interactúas con la documentación y revisas el response, aunque el código no agregó nada en uno de los campos description, el response JSON contiene el valor por defecto (null):

Esto significa que siempre tendrá un valor, solo que a veces el valor podría ser None (o null en JSON).

Eso significa que, los clientes que usan tu API no tienen que comprobar si el valor existe o no, pueden asumir que el campo siempre estará allí, pero solo que en algunos casos tendrá el valor por defecto de None.

La forma de describir esto en OpenAPI es marcar ese campo como requerido, porque siempre estará allí.

Debido a eso, el JSON Schema para un modelo puede ser diferente dependiendo de si se usa para entrada o salida:

  • para entrada el description no será requerido
  • para salida será requerido (y posiblemente None, o en términos de JSON, null)

Modelo para Salida en la Documentación

También puedes revisar el modelo de salida en la documentación, ambos name y description están marcados como requeridos con un asterisco rojo:

Modelo para Entrada y Salida en la Documentación

Y si revisas todos los esquemas disponibles (JSON Schemas) en OpenAPI, verás que hay dos, uno Item-Input y uno Item-Output.

Para Item-Input, description no es requerido, no tiene un asterisco rojo.

Pero para Item-Output, description es requerido, tiene un asterisco rojo.

Con esta funcionalidad de Pydantic v2, la documentación de tu API es más precisa, y si tienes clientes y SDKs autogenerados, también serán más precisos, con una mejor experiencia para desarrolladores y consistencia. 🎉

No Separar Esquemas

Ahora, hay algunos casos donde podrías querer tener el mismo esquema para entrada y salida.

Probablemente el caso principal para esto es si ya tienes algún código cliente/SDKs autogenerado y no quieres actualizar todo el código cliente/SDKs autogenerado aún, probablemente querrás hacerlo en algún momento, pero tal vez no ahora.

En ese caso, puedes desactivar esta funcionalidad en FastAPI, con el parámetro separate_input_output_schemas=False.

Información

El soporte para separate_input_output_schemas fue agregado en FastAPI 0.102.0. 🤓

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: str | None = None


app = FastAPI(separate_input_output_schemas=False)


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


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
🤓 Other versions and variants
from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Optional[str] = None


app = FastAPI(separate_input_output_schemas=False)


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


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
from typing import List, Union

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Union[str, None] = None


app = FastAPI(separate_input_output_schemas=False)


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


@app.get("/items/")
def read_items() -> List[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]

Mismo Esquema para Modelos de Entrada y Salida en la Documentación

Y ahora habrá un único esquema para entrada y salida para el modelo, solo Item, y tendrá description como no requerido:

Este es el mismo comportamiento que en Pydantic v1. 🤓