Pydantic v2 en FastAPI

Intermedio
FastAPI
FastAPI
Actualizado: 18/04/2026

Diagrama: Fastapi pydantic v2

Pydantic v2: la base de FastAPI modernizada

Pydantic es la librería que FastAPI utiliza internamente para validar todos los datos: bodies de peticiones, parámetros de ruta, query params y modelos de respuesta. La versión 2 de Pydantic fue publicada en 2023 e introduce mejoras sustanciales en rendimiento (hasta 50x más rápida en algunos casos) y en la API de definición de modelos.

FastAPI a partir de la versión 0.100.0 incluye soporte nativo para Pydantic v2, y es la versión activa en 2026.

Cambios principales respecto a Pydantic v1

model_config en lugar de la clase Config

En Pydantic v1, la configuración del modelo se hacía con una clase interna Config. En Pydantic v2 se usa el atributo de clase model_config:

# Pydantic v1 (obsoleto)
class UsuarioV1(BaseModel):
    nombre: str
    email: str

    class Config:
        orm_mode = True
        str_strip_whitespace = True

# Pydantic v2 (forma actual)
from pydantic import BaseModel, ConfigDict

class Usuario(BaseModel):
    model_config = ConfigDict(
        from_attributes=True,       # Antes: orm_mode = True
        str_strip_whitespace=True,
        str_min_length=1,
    )

    nombre: str
    email: str

from_attributes en lugar de orm_mode

El cambio más relevante para FastAPI con SQLAlchemy es que orm_mode = True se renombra a from_attributes = True:

from pydantic import BaseModel, ConfigDict
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

# Modelo SQLAlchemy (sin cambios)
class ProductoModel(Base):
    __tablename__ = "productos"
    id = Column(Integer, primary_key=True)
    nombre = Column(String(100))
    precio = Column(Integer)

# Schema Pydantic v2 para respuestas
class ProductoSchema(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    nombre: str
    precio: float

Con from_attributes=True, Pydantic puede leer los atributos de objetos ORM directamente, no solo de diccionarios.

@field_validator: el nuevo validador de campos

En Pydantic v2, el decorador @validator fue reemplazado por @field_validator, con una API más clara y explícita:

from pydantic import BaseModel, field_validator

class Producto(BaseModel):
    nombre: str
    precio: float
    stock: int

    @field_validator("nombre")
    @classmethod
    def nombre_no_vacio(cls, v: str) -> str:
        v = v.strip()
        if len(v) < 2:
            raise ValueError("El nombre debe tener al menos 2 caracteres")
        return v.title()  # Convierte a Title Case

    @field_validator("precio")
    @classmethod
    def precio_positivo(cls, v: float) -> float:
        if v <= 0:
            raise ValueError("El precio debe ser mayor que cero")
        return round(v, 2)  # Redondear a 2 decimales

    @field_validator("stock")
    @classmethod
    def stock_no_negativo(cls, v: int) -> int:
        if v < 0:
            raise ValueError("El stock no puede ser negativo")
        return v

mode="before" vs mode="after"

Los validadores pueden ejecutarse antes o después de la conversión de tipos:

from pydantic import BaseModel, field_validator

class Pedido(BaseModel):
    codigo: str
    cantidad: int

    # Se ejecuta ANTES de la conversión de tipos
    # Recibe el valor tal como llegó (puede ser str, int, etc.)
    @field_validator("cantidad", mode="before")
    @classmethod
    def convertir_cantidad(cls, v):
        # Acepta "5 unidades" y extrae el número
        if isinstance(v, str):
            return v.split()[0]  # Extrae "5" de "5 unidades"
        return v

    # Se ejecuta DESPUÉS de la conversión de tipos
    # Siempre recibe el tipo ya validado
    @field_validator("codigo", mode="after")
    @classmethod
    def normalizar_codigo(cls, v: str) -> str:
        return v.upper().strip()

Validar múltiples campos con un solo validador

from pydantic import BaseModel, field_validator
from typing import Annotated

class Rango(BaseModel):
    minimo: float
    maximo: float

    @field_validator("minimo", "maximo", mode="after")
    @classmethod
    def no_negativos(cls, v: float) -> float:
        if v < 0:
            raise ValueError("Los valores no pueden ser negativos")
        return v

@model_validator: validación entre campos

Cuando necesitas validar la relación entre varios campos, usa @model_validator:

from pydantic import BaseModel, model_validator
from datetime import date

class Reserva(BaseModel):
    fecha_entrada: date
    fecha_salida: date
    precio_por_noche: float

    @model_validator(mode="after")
    def validar_fechas(self) -> "Reserva":
        if self.fecha_salida <= self.fecha_entrada:
            raise ValueError("La fecha de salida debe ser posterior a la de entrada")
        return self

    @property
    def noches(self) -> int:
        return (self.fecha_salida - self.fecha_entrada).days

    @property
    def precio_total(self) -> float:
        return self.noches * self.precio_por_noche

Field() con metadata avanzada

Field() permite enriquecer los campos con validaciones declarativas, ejemplos y metadata para la documentación OpenAPI:

from pydantic import BaseModel, Field
from typing import Annotated

class Usuario(BaseModel):
    nombre: Annotated[str, Field(
        min_length=2,
        max_length=50,
        description="Nombre completo del usuario",
        examples=["Ana García", "Carlos López"]
    )]
    email: Annotated[str, Field(
        pattern=r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
        description="Correo electrónico válido",
        examples=["usuario@ejemplo.com"]
    )]
    edad: Annotated[int, Field(
        ge=0,
        le=150,
        description="Edad en años"
    )]
    puntuacion: Annotated[float, Field(
        ge=0.0,
        le=10.0,
        description="Puntuación entre 0 y 10"
    )]

Los valores de ge (mayor o igual), le (menor o igual), gt (mayor que), lt (menor que) y pattern se reflejan automáticamente en la documentación Swagger UI y en el esquema OpenAPI.

Serialización: model_dump() y model_dump_json()

En Pydantic v2, dict() y json() fueron reemplazados por model_dump() y model_dump_json():

from pydantic import BaseModel
from datetime import datetime

class Evento(BaseModel):
    titulo: str
    fecha: datetime
    activo: bool = True

evento = Evento(titulo="FastAPI Workshop", fecha=datetime.now())

# Antes (v1): evento.dict()
datos = evento.model_dump()
print(datos)
# {'titulo': 'FastAPI Workshop', 'fecha': datetime(...), 'activo': True}

# Serialización JSON
json_str = evento.model_dump_json()
print(json_str)
# {"titulo":"FastAPI Workshop","fecha":"2026-03-30T...","activo":true}

# Opciones útiles
datos_parciales = evento.model_dump(exclude={"fecha"})
datos_sin_nulos = evento.model_dump(exclude_none=True)
datos_incluidos = evento.model_dump(include={"titulo", "activo"})

Modelos de respuesta en FastAPI con Pydantic v2

FastAPI usa Pydantic v2 automáticamente. Solo hay que asegurarse de usar la nueva sintaxis:

from fastapi import FastAPI
from pydantic import BaseModel, ConfigDict, Field
from typing import Annotated

app = FastAPI()

class ProductoCrear(BaseModel):
    nombre: Annotated[str, Field(min_length=2, max_length=100)]
    precio: Annotated[float, Field(gt=0)]
    descripcion: str | None = None

class ProductoRespuesta(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    nombre: str
    precio: float
    descripcion: str | None = None

@app.post("/productos/", response_model=ProductoRespuesta, status_code=201)
async def crear_producto(producto: ProductoCrear):
    # Simula guardado en BD y retorno del objeto
    return {"id": 1, **producto.model_dump()}

@app.get("/productos/{producto_id}", response_model=ProductoRespuesta)
async def obtener_producto(producto_id: int):
    return {"id": producto_id, "nombre": "Laptop", "precio": 999.99}

Manejo de campos opcionales con None vs ausencia

Pydantic v2 distingue correctamente entre un campo con valor None y un campo no enviado en el body:

from pydantic import BaseModel
from typing import Optional

class ActualizarUsuario(BaseModel):
    nombre: Optional[str] = None
    email: Optional[str] = None
    bio: Optional[str] = None

# Ejemplo de uso en endpoint PATCH
@app.patch("/usuarios/{usuario_id}")
async def actualizar_usuario(usuario_id: int, datos: ActualizarUsuario):
    # model_dump(exclude_unset=True) solo incluye los campos
    # que el cliente envió explícitamente
    campos_a_actualizar = datos.model_dump(exclude_unset=True)
    print(campos_a_actualizar)
    # Si el cliente envió {"nombre": "Ana"}, solo aparece {"nombre": "Ana"}
    # El email y la bio no se sobrescriben porque no fueron enviados
    return {"id": usuario_id, **campos_a_actualizar}

El uso de exclude_unset=True en model_dump() es un patrón esencial para implementar operaciones PATCH correctas en FastAPI.

Fuentes y referencias

Documentación oficial y recursos externos para profundizar en FastAPI

Documentación oficial de FastAPI
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, FastAPI es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de FastAPI

Explora más contenido relacionado con FastAPI y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Comprender las diferencias clave entre Pydantic v1 y v2 relevantes para FastAPI. Usar @field_validator y @model_validator para validaciones personalizadas en Pydantic v2. Configurar modelos con model_config en lugar de la clase Config interna. Aprovechar from_attributes (antes orm_mode) para integrar modelos con SQLAlchemy. Usar Field() con metadata avanzada para anotaciones, ejemplos y validaciones declarativas.