Bloque II · Módulo 3 · Unidad 2.3
Pydantic 2 como sistema de contratos
Modelar datos anidados, restricciones y reglas cruzadas con Pydantic 2, separando creación, actualización y salida.
1. Un modelo representa datos validados#
Pydantic toma datos no confiables, intenta validarlos según anotaciones y reglas, y produce una instancia cuyo estado cumple ese contrato. No promete conservar el tipo exacto de la entrada: en modo no estricto puede convertir valores compatibles.
from datetime import date
from pydantic import BaseModel, Field
class Record(BaseModel):
reference: str = Field(pattern=r"^EXP-\d{3}$")
opened_on: date
priority: int = Field(ge=1, le=5)
record = Record.model_validate({
"reference": "EXP-204",
"opened_on": "2026-07-27",
"priority": "3",
})
model_validate es la entrada explícita de Pydantic 2; model_dump convierte la instancia a datos Python. Para JSON existen model_validate_json y model_dump_json.
2. Field y ConfigDict#
Field declara restricciones locales y metadatos:
from pydantic import BaseModel, ConfigDict, Field
class Department(BaseModel):
model_config = ConfigDict(extra="forbid", strict=True)
code: str = Field(min_length=2, max_length=12, pattern=r"^[A-Z0-9-]+$")
capacity: int = Field(gt=0, le=500)
extra="forbid" rechaza claves desconocidas. El default de Pydantic es ignorarlas; elegir explícitamente evita que un typo desaparezca sin señal. strict=True reduce conversiones, aunque también puede configurarse por campo o durante una validación concreta.
| Opción | Efecto | Pregunta de diseño |
|---|---|---|
extra="ignore" | Descarta extras. | ¿La compatibilidad tolerante supera el riesgo de typos? |
extra="forbid" | Rechaza extras. | ¿Quieres un contrato cerrado y feedback temprano? |
strict=True | Reduce coerciones. | ¿Debe "3" rechazarse donde se espera 3? |
validate_assignment=True | Valida al reasignar. | ¿La instancia será mutable después de crearla? |
3. Modelos anidados y enums#
from enum import StrEnum
class RecordState(StrEnum):
draft = "draft"
in_review = "in_review"
closed = "closed"
class Contact(BaseModel):
name: str = Field(min_length=2)
email: str
class RecordCreate(BaseModel):
state: RecordState = RecordState.draft
owner: Contact
tags: list[str] = Field(default_factory=list, max_length=10)
Los modelos anidados conservan estructura y generan esquemas reutilizables en OpenAPI. Un enum cierra el vocabulario y evita cadenas “casi válidas”. default_factory=list crea una lista por instancia.
4. Elegir el nivel correcto de validador#
Una restricción declarativa en Field es preferible para longitud, rango o patrón. Un field_validator sirve cuando la regla pertenece a un campo pero necesita lógica propia:
from pydantic import field_validator
class RecordCreate(BaseModel):
reference: str
@field_validator("reference")
@classmethod
def normalize_reference(cls, value: str) -> str:
normalized = value.strip().upper()
if not normalized.startswith("EXP-"):
raise ValueError("must start with EXP-")
return normalized
Una regla que compara varios campos pertenece a un model_validator:
from typing import Self
from pydantic import model_validator
class RecordPeriod(BaseModel):
opened_on: date
closed_on: date | None = None
@model_validator(mode="after")
def check_period(self) -> Self:
if self.closed_on is not None and self.closed_on < self.opened_on:
raise ValueError("closed_on cannot precede opened_on")
return self
Un validador after recibe la instancia ya validada y debe devolverla. Lanza ValueError para errores de entrada esperables; un TypeError suele señalar un bug de programación y no se convierte igual.
5. Crear, actualizar y responder son contratos distintos#
class RecordBase(BaseModel):
subject: str = Field(min_length=3, max_length=120)
priority: int = Field(ge=1, le=5)
class RecordCreate(RecordBase):
owner: Contact
class RecordUpdate(BaseModel):
subject: str | None = Field(default=None, min_length=3, max_length=120)
priority: int | None = Field(default=None, ge=1, le=5)
class RecordPublic(RecordBase):
id: str
state: RecordState
El contrato de creación contiene lo que puede enviar el cliente. El de actualización parcial hace opcionales únicamente los campos modificables. El de salida contiene lo que se compromete a publicar el servidor. Reutilizar un único modelo suele admitir entradas que deberían estar prohibidas o filtrar mal datos internos.
changes = patch.model_dump(exclude_unset=True)
exclude_unset=True conserva la diferencia entre omitido y presente con null.
6. Serialización y alias#
from pydantic import AliasChoices
class ExternalRecord(BaseModel):
model_config = ConfigDict(
validate_by_name=True,
validate_by_alias=True,
serialize_by_alias=True,
)
internal_id: str = Field(
validation_alias=AliasChoices("internal_id", "recordId"),
serialization_alias="recordId",
)
En Pydantic 2.11+, validate_by_name=True junto con validate_by_alias=True expresa el comportamiento antes asociado a populate_by_name. serialization_alias controla la salida; validation_alias controla entradas aceptadas. Un alias es una decisión de compatibilidad pública, no un modo de ocultar un mal nombre interno.
7. Errores y límites del modelo#
Un error de Pydantic incluye una ruta dentro de la estructura:
owner.name
String should have at least 2 characters
La localización puede atravesar objetos y listas, por ejemplo ("items", 2, "code"). Esta precisión es parte del contrato diagnóstico.
No pertenecen al modelo de entrada:
- consultar si una referencia ya existe;
- comprobar permisos del usuario;
- escribir en base de datos;
- enviar correo;
- leer secretos o configuración global.
Esas operaciones dependen de estado externo y se incorporarán cuando exista una arquitectura que las sostenga.
8. Criterios de dominio#
- Usas APIs y vocabulario de Pydantic 2.
- Justificas coerción o modo estricto.
- Modelas estructuras anidadas y vocabularios cerrados.
- Distingues
Field,field_validatorymodel_validator. - Separas create, update y public.
- Conservas presencia mediante
model_fields_setoexclude_unset. - Mantienes estado externo fuera de validadores.
Prácticas relacionadas#
- EX-B2-07 · Field o validador
- EX-B2-08 · Modelos anidados y enums
- EX-B2-09 · Create, update y public
- EX-B2-S01 · Admisiones, parte 2