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. 🤓