Bloque II · Módulo 2 · Unidad 2.2
Entrada de datos HTTP y validación automática
Declarar de dónde procede cada dato de una petición y comprender conversión, obligatoriedad, ausencia, null y errores 422.
1. Una firma es un contrato#
FastAPI interpreta nombres, tipos y metadatos de la firma. Cada parámetro declara dos decisiones diferentes: de dónde se extrae el dato y qué valores se aceptan.
from typing import Annotated
from fastapi import FastAPI, Header, Path, Query
app = FastAPI()
@app.get("/expedientes/{expediente_id}")
def read_record(
expediente_id: Annotated[str, Path(pattern=r"^EXP-\d{3}$")],
page: Annotated[int, Query(ge=1)] = 1,
correlation_id: Annotated[str | None, Header(alias="X-Correlation-ID")] = None,
):
return {"id": expediente_id, "page": page, "correlation_id": correlation_id}
Annotated[T, Query(...)] conserva T como tipo para Python y adjunta metadatos para FastAPI. El valor por defecto sigue expresando obligatoriedad: sin default es requerido; = 1 lo hace opcional con default; = None permite ausencia.
2. Path y query#
El path identifica el recurso o subrecurso que se está direccionando. La query modifica una vista: filtra, pagina, ordena o incluye representaciones opcionales.
| Pregunta | Path | Query |
|---|---|---|
| ¿Qué expresa? | Identidad dentro de la ruta. | Opciones sobre la representación o colección. |
| ¿Puede omitirse? | No si forma parte del patrón. | Sí, cuando existe default. |
| Ejemplo | /expedientes/EXP-204 | ?estado=abierto&page=2 |
| Error de diseño | Meter un filtro cambiante en la jerarquía. | Usarla para identificar ambiguamente el recurso principal. |
@app.get("/expedientes")
def list_records(
estado: Annotated[str | None, Query(pattern="^(abierto|cerrado)$")] = None,
limit: Annotated[int, Query(ge=1, le=100)] = 20,
):
...
Los valores de query llegan como texto. FastAPI los convierte según el tipo y produce un error de validación si la conversión o las restricciones fallan.
3. Headers y cookies#
Los headers transportan metadatos de la interacción: negociación, autenticación, trazabilidad o condiciones. Las cookies son pares gestionados por el cliente para un dominio y deben usarse con cautela.
from fastapi import Cookie
@app.get("/preferencias")
def preferences(
correlation_id: Annotated[str, Header(alias="X-Correlation-ID")],
display_mode: Annotated[str | None, Cookie(alias="display_mode")] = None,
):
...
FastAPI convierte normalmente guiones bajos de un parámetro de header en guiones. Un alias explícito hace visible el nombre público y evita depender de esa convención.
4. Body JSON#
Un modelo Pydantic en la firma se interpreta como body JSON:
from pydantic import BaseModel, Field
class RecordCreate(BaseModel):
subject: str = Field(min_length=3, max_length=120)
priority: int = Field(ge=1, le=5)
@app.post("/expedientes")
def create_record(payload: RecordCreate):
return payload
El modelo agrupa campos relacionados de una representación. No uses un body para todos los datos: un identificador que direcciona el recurso sigue perteneciendo al path; el correlation ID sigue siendo un header.
5. Formulario y archivo#
Los formularios HTML envían application/x-www-form-urlencoded o multipart/form-data. Los archivos requieren multipart:
from fastapi import File, Form, UploadFile
@app.post("/importaciones")
async def import_record(
category: Annotated[str, Form()],
document: Annotated[UploadFile, File()],
):
content = await document.read(1024)
return {"category": category, "filename": document.filename, "sample_bytes": len(content)}
UploadFile ofrece metadatos y un archivo temporal con interfaz asíncrona; evita cargar de inmediato todo el contenido como bytes. Debes imponer límites reales en una aplicación de producción, pero ese diseño pertenece a un alcance posterior.
6. Ausente, null y default no son lo mismo#
Considera:
class RecordPatch(BaseModel):
title: str | None = None
Este tipo permite que title se omita y que se envíe null; en ambos casos el atributo vale None. Para una actualización parcial necesitas además saber si el campo fue enviado:
payload.model_fields_set
payload.model_dump(exclude_unset=True)
| Entrada | Presencia | Valor | Uso típico |
|---|---|---|---|
{} | Ausente | Default en el modelo | No cambiar el campo. |
{"title": null} | Presente | None | Vaciarlo si el dominio lo permite. |
{"title": "Nueva"} | Presente | Texto validado | Reemplazarlo. |
El tipo str | None permite None; no convierte por sí solo el campo en opcional. La presencia se decide con el default:
required_but_nullable: str | None
optional_and_nullable: str | None = None
7. Leer un error de validación#
Cuando path, query, header o body no cumplen el contrato, FastAPI genera una respuesta 422. Cada elemento incluye una ubicación (loc), un mensaje (msg), un tipo de error y, según el caso, contexto e input.
{
"detail": [
{
"type": "greater_than_equal",
"loc": ["query", "page"],
"msg": "Input should be greater than or equal to 1",
"input": "0",
"ctx": {"ge": 1}
}
]
}
La ubicación permite corregir el cliente sin adivinar: ["query", "page"] no es lo mismo que ["body", "page"].
8. Diseñar la frontera antes de codificar#
Para cada dato pregunta:
- ¿identifica el recurso? → path;
- ¿filtra o pagina? → query;
- ¿describe la interacción? → header;
- ¿es una preferencia pequeña del cliente? → cookie;
- ¿forma una representación estructurada? → body JSON;
- ¿procede de un formulario o acompaña un archivo? → form/multipart.
Después declara tipo, obligatoriedad, límites y ejemplo. Solo entonces escribe la operación.
9. Criterios de dominio#
- Justificas la fuente HTTP de cada dato.
- Predices si un parámetro es requerido, opcional o nullable.
- Distingues JSON, formulario y multipart.
- Lees
locy reconoce qué validación falló. - Preservas la diferencia entre omitido y
null. - Evitas transportar secretos en cookies o errores.
Prácticas relacionadas#
- EX-B2-03 · Elegir la fuente HTTP
- EX-B2-04 · Correlación y preferencia
- EX-B2-05 · JSON, formulario o archivo
- EX-B2-06 · Ausente, null y default