Pydantic
2.3 Modelado y validación con Pydantic
2.3.1 Creación de esquemas (BaseModel)
Pydantic se basa en clases que heredan de BaseModel. Cada atributo que definimos se convierte en un campo con reglas estrictas.
- Campos obligatorios vs. opcionales: un campo es obligatorio si solo defines su tipo. Es opcional si le asignas un valor por defecto, como
None, un texto fijo o un número. - Modelos anidados: puedes usar un modelo de Pydantic como tipo de dato de un atributo dentro de otro modelo. Esto permite estructurar JSON con subobjetos complejos.
- Tipos especiales: Pydantic incluye tipos avanzados listos para usar. Algunos requieren instalar
pydantic[email].- EmailStr: valida que el texto sea un correo electrónico real, por ejemplo:
usuario@dominio.com. - HttpUrl: valida que sea una URL web válida.
- datetime: convierte automáticamente cadenas en formato ISO, como
2026-09-25T12:00:00, a objetosdatetimede Python. - UUID y Decimal: son ideales para identificadores únicos y transacciones de dinero exactas.
- EmailStr: valida que el texto sea un correo electrónico real, por ejemplo:
from datetime import datetime
from decimal import Decimal
from uuid import UUID
from pydantic import BaseModel, EmailStr, HttpUrl
# Modelo hijo
class Direccion(BaseModel):
calle: str
ciudad: str
# Modelo padre (anidado)
class ClientePerfil(BaseModel):
id: UUID
email: EmailStr
sitio_web: HttpUrl | None = None
balance: Decimal
creado_en: datetime
direccion: Direccion
2.3.2 Validación automática y errores 422
Cuando FastAPI recibe un JSON, Pydantic intenta validarlo campo por campo.
- Coerción de tipos: Pydantic es flexible por defecto. Si defines un campo como
inty el cliente envía"5"o5.0, Pydantic lo convertirá automáticamente a5. - Restricciones con
Field(): sirve para añadir metadatos, valores por defecto y límites a nivel de atributo.
from pydantic import BaseModel, Field
class Producto(BaseModel):
nombre: str = Field(min_length=3, examples=["Laptop Gamer"])
precio: float = Field(default=1.0, gt=0, description="Precio unitario")
2.3.3 Validaciones personalizadas
Cuando las restricciones básicas de Field() no son suficientes, podemos programar nuestras propias reglas de negocio mediante decoradores de Pydantic.
@field_validator: valida un solo campo. Siempre debe ser un método de clase con@classmethod. Si el dato es incorrecto, lanza unValueError.@model_validator: valida el modelo completo después de haber validado los campos individuales. Es ideal para comparar campos entre sí.
from pydantic import BaseModel, Field, field_validator, model_validator
from typing_extensions import Self
class RegistroUsuario(BaseModel):
username: str
password: str = Field(min_length=8)
confirm_password: str
@field_validator("username")
@classmethod
def username_no_admin(cls, v: str) -> str:
if "admin" in v.lower():
raise ValueError("El nombre de usuario no puede contener la palabra 'admin'")
return v
@model_validator(mode="after")
def verificar_contraseñas(self) -> Self:
if self.password != self.confirm_password:
raise ValueError("Las contraseñas no coinciden")
return self
2.3.4 Serialización de datos
La serialización es el proceso inverso: transformar un objeto de Python/Pydantic de nuevo a un formato JSON o a un diccionario estándar para enviarlo al cliente.
.model_dump()y.model_dump_json(): convierten un modelo en un diccionario o en una cadena JSON.response_model: controla qué devuelve el endpoint.
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr, Field
app = FastAPI()
class UserCreate(BaseModel):
email: EmailStr
password: str = Field(min_length=8)
class UserRead(BaseModel):
id: int
email: EmailStr
activo: bool
@app.post("/usuarios/", response_model=UserRead)
def crear_usuario(usuario_ingresado: UserCreate):
usuario_db = {"id": 1, "email": usuario_ingresado.email, "activo": True}
return usuario_db
En este ejemplo, aunque la función devuelve un diccionario con más información, FastAPI solo expondrá los campos definidos en
UserRead.
response_model_exclude_unset=True: si la base de datos tiene campos vacíos o por defecto que el cliente no envió, esta opción evita enviar esos campos vacíos en el JSON final, reduciendo el ancho de banda.- Alias de campos: permite mapear nombres de Python, como
snake_case, con nombres de JSON o JavaScript, comocamelCase.
Patrón de esquemas
Es común trabajar con tres tipos de modelos:
- UserCreate: para la entrada del cliente.
- UserRead: para la salida pública del sistema.
- UserUpdate: para actualizaciones parciales.
2.3.5 Configuración de modelos (model_config)
Pydantic nos permite modificar el comportamiento de un modelo con una variable de clase especial llamada model_config, que usa ConfigDict.
La configuración más importante cuando trabajas con APIs es from_attributes=True (antes llamada orm_mode=True).
¿Por qué es clave from_attributes=True?
Por defecto, Pydantic sabe leer diccionarios de Python, como usuario["nombre"]. Sin embargo, las bases de datos usando ORMs, como SQLAlchemy o SQLModel, suelen devolver objetos con atributos, como usuario.nombre.
Al activar from_attributes=True, Pydantic puede tomar un objeto directamente de la base de datos, extraer sus atributos de forma automática y serializarlos en el JSON de salida sin necesidad de hacer un mapeo manual campo por campo.
from pydantic import BaseModel, ConfigDict
class Usuario(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
nombre: str
Esto es muy útil cuando más adelante trabajemos con ORM y bases de datos en FastAPI.