Elegir espacio

Dos espacios

¿Qué quieres consultar?

Elige el espacio al que quieres entrar.

Recorrido · FastAPIBloque II · Unidad 2.3

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ónEfectoPregunta 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=TrueReduce coerciones.¿Debe "3" rechazarse donde se espera 3?
validate_assignment=TrueValida 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_validator y model_validator.
  • Separas create, update y public.
  • Conservas presencia mediante model_fields_set o exclude_unset.
  • Mantienes estado externo fuera de validadores.

Prácticas relacionadas#

Fuentes#