FastAPI
Tutorial FastAPI: Validación de datos con Pydantic 2
Aprende a validar y gestionar datos con Pydantic 2 en Python, integrando modelos robustos y validaciones avanzadas para APIs con FastAPI.
Aprende FastAPI y certifícateIntroducción a Pydantic y modelos de datos
Pydantic es una biblioteca de Python que facilita la validación de datos y la gestión de configuraciones mediante la definición de modelos basados en tipos. En el contexto de FastAPI, Pydantic se convierte en una herramienta fundamental para validar los datos que reciben nuestros endpoints y para serializar las respuestas que enviamos a los clientes.
¿Qué es Pydantic?
Pydantic es una biblioteca de validación que aprovecha las anotaciones de tipo de Python para proporcionar validación de datos en tiempo de ejecución. A diferencia de otras soluciones, Pydantic valida los datos durante la creación de instancias de modelos, lo que garantiza que los datos sean consistentes y cumplan con las restricciones definidas.
La versión 2 de Pydantic, que utilizaremos en este curso, introduce mejoras significativas en rendimiento y nuevas funcionalidades que hacen que la validación de datos sea aún más robusta y flexible.
Instalación de Pydantic
Para comenzar a trabajar con Pydantic, necesitamos instalarlo en nuestro entorno:
pip install pydantic
FastAPI ya incluye Pydantic como dependencia, por lo que si has instalado FastAPI, probablemente ya tengas Pydantic disponible. Sin embargo, para asegurarte de tener la versión 2, puedes actualizarla:
pip install -U pydantic
Modelos básicos en Pydantic
Un modelo en Pydantic es simplemente una clase que hereda de BaseModel
. Esta clase define la estructura de los datos que queremos validar, utilizando anotaciones de tipo de Python.
Veamos un ejemplo básico:
from pydantic import BaseModel
from typing import Optional
class User(BaseModel):
id: int
name: str
email: str
active: bool = True
bio: Optional[str] = None
En este ejemplo:
- Creamos un modelo
User
que hereda deBaseModel
- Definimos campos con sus tipos esperados (int, str, bool)
- Establecemos valores predeterminados para algunos campos (active = True)
- Utilizamos
Optional
para indicar que un campo puede serNone
Creación y validación de instancias
Una vez definido el modelo, podemos crear instancias y Pydantic se encargará de validar automáticamente los datos:
# Datos válidos
user1 = User(id=1, name="Ana García", email="ana@ejemplo.com")
print(user1)
# User(id=1, name='Ana García', email='ana@ejemplo.com', active=True, bio=None)
# Datos inválidos - intentando asignar un string a un campo int
try:
user2 = User(id="no-es-un-numero", name="Carlos López", email="carlos@ejemplo.com")
except Exception as e:
print(f"Error: {e}")
# Error: 1 validation error for User
# id
# Input should be a valid integer [type=int_type, input_value='no-es-un-numero', input_type=str]
Pydantic no solo valida los tipos, sino que también intenta convertir los datos al tipo esperado cuando es posible:
# Conversión automática de tipos
user3 = User(id="42", name="Elena Martínez", email="elena@ejemplo.com")
print(user3)
# User(id=42, name='Elena Martínez', email='elena@ejemplo.com', active=True, bio=None)
print(type(user3.id)) # <class 'int'> - Pydantic ha convertido el string "42" a int
Modelos anidados
Pydantic permite definir modelos anidados, lo que es muy útil para representar estructuras de datos complejas:
from pydantic import BaseModel
from typing import List, Optional
class Address(BaseModel):
street: str
city: str
country: str
postal_code: str
class User(BaseModel):
id: int
name: str
email: str
addresses: List[Address] = []
primary_address: Optional[Address] = None
Podemos crear instancias con datos anidados:
# Creando un usuario con direcciones
user = User(
id=1,
name="Miguel Sánchez",
email="miguel@ejemplo.com",
addresses=[
{"street": "Calle Mayor 10", "city": "Madrid", "country": "España", "postal_code": "28001"},
{"street": "Avenida Libertad 25", "city": "Barcelona", "country": "España", "postal_code": "08001"}
],
primary_address={"street": "Calle Mayor 10", "city": "Madrid", "country": "España", "postal_code": "28001"}
)
print(user.primary_address.city) # Madrid
print(len(user.addresses)) # 2
Serialización y deserialización
Pydantic facilita la conversión entre objetos Python y formatos como JSON, lo que es esencial en el desarrollo de APIs:
# Serialización a diccionario
user_dict = user.model_dump()
print(type(user_dict)) # <class 'dict'>
# Serialización a JSON
user_json = user.model_dump_json()
print(type(user_json)) # <class 'str'>
print(user_json[:50]) # {"id":1,"name":"Miguel Sánchez","email":"miguel@eje...
# Deserialización desde diccionario
new_user = User.model_validate(user_dict)
print(type(new_user)) # <class 'User'>
Integración con FastAPI
En FastAPI, los modelos de Pydantic se utilizan para:
- Validar datos de entrada: Definiendo los parámetros de las rutas
- Documentar la API: Generando automáticamente la documentación OpenAPI
- Serializar respuestas: Convirtiendo objetos Python a JSON
Veamos un ejemplo básico de cómo se integra Pydantic con FastAPI:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = 0.0
@app.post("/items/")
async def create_item(item: Item):
# FastAPI valida automáticamente que los datos recibidos
# cumplan con el modelo Item
# Podemos acceder a los campos validados
price_with_tax = item.price + item.tax
# Y devolver un diccionario o un modelo Pydantic
return {
"item_name": item.name,
"price_with_tax": price_with_tax
}
En este ejemplo:
- Definimos un modelo
Item
con Pydantic - FastAPI usa este modelo para validar automáticamente el cuerpo de la solicitud POST
- Si los datos no son válidos, FastAPI devuelve un error 422 con detalles sobre la validación fallida
- Si los datos son válidos, podemos acceder a los campos ya validados y convertidos
Ventajas de usar Pydantic en FastAPI
El uso de Pydantic en FastAPI proporciona varias ventajas clave:
- Validación automática: No necesitas escribir código para validar manualmente los datos
- Conversión de tipos: Los datos se convierten automáticamente a los tipos Python adecuados
- Documentación automática: FastAPI genera documentación OpenAPI basada en tus modelos
- Serialización/deserialización: Facilita la conversión entre JSON y objetos Python
- Seguridad: Ayuda a prevenir errores y vulnerabilidades relacionadas con datos incorrectos
Modelos para respuestas
También podemos usar modelos Pydantic para definir la estructura de las respuestas:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
app = FastAPI()
class ItemBase(BaseModel):
name: str
description: str = None
price: float
tax: float = 0.0
class ItemCreate(ItemBase):
# Modelo para la creación - hereda de ItemBase
pass
class Item(ItemBase):
# Modelo para la respuesta - añade un id
id: int
@app.post("/items/", response_model=Item)
async def create_item(item: ItemCreate):
# Simulamos la creación de un item en la base de datos
# y le asignamos un id
new_item = Item(
id=1,
name=item.name,
description=item.description,
price=item.price,
tax=item.tax
)
return new_item
El parámetro response_model
le indica a FastAPI que debe validar y filtrar la respuesta según el modelo especificado, lo que garantiza que la respuesta tenga la estructura esperada.
Configuración de modelos
Pydantic permite configurar el comportamiento de los modelos mediante la clase Config
:
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
email: str
password: str
class Config:
# Excluir campos sensibles al serializar a JSON
json_schema_extra = {
"example": {
"id": 1,
"name": "Usuario Ejemplo",
"email": "usuario@ejemplo.com",
"password": "contraseña123"
}
}
# No incluir valores None en la serialización
exclude_none = True
# Permitir nombres de campo con guiones bajos en JSON
populate_by_name = True
Esta configuración afecta a cómo se comporta el modelo al validar datos y al serializarlos.
Campos computados
Pydantic 2 introduce el concepto de campos computados que permiten definir propiedades que se calculan a partir de otros campos:
from pydantic import BaseModel, computed_field
class Product(BaseModel):
name: str
price: float
tax_rate: float = 0.21
@computed_field
def price_with_tax(self) -> float:
return self.price * (1 + self.tax_rate)
product = Product(name="Laptop", price=1000)
print(product.price_with_tax) # 1210.0
Los campos computados se incluyen en la serialización del modelo, lo que los hace útiles para enviar datos derivados en las respuestas de la API.
Tipos de datos y validaciones
Pydantic ofrece un sistema robusto de validación que va mucho más allá de la simple comprobación de tipos. En esta sección exploraremos los diferentes tipos de datos que podemos utilizar en nuestros modelos y las validaciones específicas que podemos aplicar para garantizar la integridad de los datos.
Tipos básicos de Python
Pydantic soporta todos los tipos nativos de Python, permitiéndonos definir modelos con una estructura clara y tipada:
from pydantic import BaseModel
from datetime import datetime, date
from typing import Optional
class Product(BaseModel):
id: int
name: str
price: float
is_available: bool
created_at: datetime
release_date: date
tags: list[str]
metadata: dict[str, str] = {}
discount: Optional[float] = None
Cuando creamos una instancia de este modelo, Pydantic validará automáticamente que cada campo reciba un valor del tipo correcto:
# Ejemplo de uso con datos válidos
product = Product(
id=1,
name="Teclado ergonómico",
price=89.99,
is_available=True,
created_at="2023-10-15T14:30:00", # Se convertirá automáticamente a datetime
release_date="2023-10-20", # Se convertirá automáticamente a date
tags=["ergonómico", "mecánico", "inalámbrico"]
)
print(product.created_at) # datetime.datetime(2023, 10, 15, 14, 30)
print(type(product.created_at)) # <class 'datetime.datetime'>
Validaciones con Field
Para aplicar restricciones adicionales a los campos, Pydantic proporciona la función Field
, que permite definir validaciones específicas:
from pydantic import BaseModel, Field
class Product(BaseModel):
id: int = Field(gt=0, description="ID único del producto")
name: str = Field(min_length=3, max_length=50)
price: float = Field(gt=0, lt=10000)
stock: int = Field(ge=0, le=1000)
description: str = Field(default="", max_length=1000)
sku: str = Field(pattern=r"^[A-Z]{2}-\d{4}$")
En este ejemplo:
gt
ylt
: Mayor que (greater than) y menor que (less than)ge
yle
: Mayor o igual que (greater or equal) y menor o igual que (less or equal)min_length
ymax_length
: Longitud mínima y máxima para stringspattern
: Expresión regular que debe cumplir el stringdescription
: Documentación del campo (útil para la generación de OpenAPI)
Tipos complejos y genéricos
Pydantic soporta tipos genéricos de Python, lo que nos permite definir estructuras de datos más complejas:
from pydantic import BaseModel
from typing import List, Dict, Set, Tuple, Union
class ComplexModel(BaseModel):
tags: List[str] # Lista de strings
counts: Dict[str, int] # Diccionario con claves string y valores enteros
unique_ids: Set[int] # Conjunto de enteros
coordinates: Tuple[float, float] # Tupla de dos flotantes
value: Union[int, str] # Puede ser entero o string
En Python 3.9+, podemos usar la sintaxis simplificada:
class ComplexModel(BaseModel):
tags: list[str]
counts: dict[str, int]
unique_ids: set[int]
coordinates: tuple[float, float]
value: int | str # Union simplificado en Python 3.10+
Validaciones personalizadas con validators
Para casos donde necesitamos lógica de validación personalizada, podemos usar decoradores @field_validator
(para un campo específico) o @model_validator
(para validar el modelo completo):
from pydantic import BaseModel, field_validator, model_validator
class User(BaseModel):
username: str
email: str
password: str
password_confirm: str
@field_validator('username')
@classmethod
def username_alphanumeric(cls, v: str) -> str:
if not v.isalnum():
raise ValueError('El nombre de usuario debe ser alfanumérico')
return v
@field_validator('email')
@classmethod
def email_domain(cls, v: str) -> str:
if not v.endswith(('.com', '.es', '.org')):
raise ValueError('Dominio de correo no válido')
return v
@model_validator(mode='after')
def check_passwords_match(self) -> 'User':
if self.password != self.password_confirm:
raise ValueError('Las contraseñas no coinciden')
return self
Observa que:
- Los validadores de campo reciben el valor a validar y deben devolver el valor (posiblemente modificado)
- El validador de modelo recibe la instancia completa y puede acceder a múltiples campos
- Usamos
@classmethod
para los validadores de campo, ya que se ejecutan antes de crear la instancia - El parámetro
mode='after'
indica que el validador se ejecuta después de que todos los campos han sido validados
Tipos especiales de Pydantic
Pydantic proporciona tipos especializados para validaciones comunes:
from pydantic import (
BaseModel, EmailStr, HttpUrl,
SecretStr, PositiveInt, NegativeFloat,
constr, confloat, conint
)
class SpecialTypes(BaseModel):
email: EmailStr # Valida formato de email
website: HttpUrl # Valida URL HTTP/HTTPS
password: SecretStr # Oculta el valor en la representación string
count: PositiveInt # Entero positivo
temperature: NegativeFloat # Flotante negativo
# Tipos con restricciones
username: constr(min_length=3, max_length=20, pattern=r'^[a-zA-Z0-9_]+$')
score: confloat(ge=0, le=100)
age: conint(ge=18, lt=120)
Para usar EmailStr
y HttpUrl
, necesitamos instalar dependencias adicionales:
pip install pydantic[email]
Enumeraciones
Las enumeraciones son útiles para restringir un campo a un conjunto específico de valores:
from enum import Enum
from pydantic import BaseModel
class UserRole(str, Enum):
ADMIN = "admin"
EDITOR = "editor"
VIEWER = "viewer"
class PaymentMethod(str, Enum):
CREDIT_CARD = "credit_card"
PAYPAL = "paypal"
BANK_TRANSFER = "bank_transfer"
class User(BaseModel):
id: int
name: str
role: UserRole = UserRole.VIEWER
class Payment(BaseModel):
amount: float
method: PaymentMethod
Al usar enumeraciones:
user = User(id=1, name="Ana", role=UserRole.ADMIN)
# También funciona con strings que coincidan con los valores de la enumeración
user2 = User(id=2, name="Carlos", role="editor")
# Esto lanzaría un error de validación
try:
user3 = User(id=3, name="Elena", role="superuser")
except Exception as e:
print(f"Error: {e}")
Tipos de fecha y hora
Pydantic maneja automáticamente la conversión y validación de fechas y horas:
from datetime import datetime, date, time, timedelta
from pydantic import BaseModel
class EventSchedule(BaseModel):
name: str
start_date: date
end_date: date
start_time: time
duration: timedelta
created_at: datetime
# Podemos proporcionar strings en formatos estándar
event = EventSchedule(
name="Conferencia Python",
start_date="2023-11-15", # ISO format
end_date="2023-11-17",
start_time="09:00:00",
duration="2:30:00", # 2 horas y 30 minutos
created_at="2023-10-01T14:30:00+02:00" # ISO format con zona horaria
)
print(event.start_date) # 2023-11-15
print(type(event.start_date)) # <class 'datetime.date'>
print(event.duration.total_seconds() / 3600) # 2.5 (horas)
Validación de listas y diccionarios
Podemos aplicar restricciones específicas a colecciones:
from pydantic import BaseModel, Field
from typing import List, Dict
class Inventory(BaseModel):
# Lista con restricción de longitud
categories: List[str] = Field(min_length=1, max_length=10)
# Diccionario con valores restringidos
stock: Dict[str, int] = Field(default_factory=dict)
@field_validator('stock')
@classmethod
def check_positive_stock(cls, v: Dict[str, int]) -> Dict[str, int]:
for item, quantity in v.items():
if quantity < 0:
raise ValueError(f"El stock de {item} no puede ser negativo")
return v
Modelos con campos discriminadores
Para casos donde necesitamos polimorfismo, Pydantic ofrece discriminadores:
from pydantic import BaseModel, Field
from typing import Literal, Union, Annotated
class BasePet(BaseModel):
pet_type: str
name: str
class Dog(BasePet):
pet_type: Literal["dog"]
breed: str
bark_loudness: int = 5
class Cat(BasePet):
pet_type: Literal["cat"]
breed: str
meow_frequency: float
# Usando discriminador
Pet = Annotated[Union[Dog, Cat], Field(discriminator='pet_type')]
def process_pet(pet: Pet):
if isinstance(pet, Dog):
print(f"{pet.name} es un perro que ladra con intensidad {pet.bark_loudness}")
elif isinstance(pet, Cat):
print(f"{pet.name} es un gato que maúlla {pet.meow_frequency} veces por hora")
# Uso
dog_data = {
"pet_type": "dog",
"name": "Rex",
"breed": "Labrador",
"bark_loudness": 8
}
cat_data = {
"pet_type": "cat",
"name": "Whiskers",
"breed": "Siamese",
"meow_frequency": 12.5
}
process_pet(Pet.model_validate(dog_data))
process_pet(Pet.model_validate(cat_data))
Validación condicional
Para implementar validaciones que dependen de otros campos, podemos combinar validadores de modelo con lógica condicional:
from pydantic import BaseModel, field_validator, model_validator
from typing import Optional
class Discount(BaseModel):
type: str # "percentage" o "fixed"
value: float
max_amount: Optional[float] = None
@model_validator(mode='after')
def check_discount_constraints(self) -> 'Discount':
if self.type == "percentage":
if not 0 <= self.value <= 100:
raise ValueError("El porcentaje debe estar entre 0 y 100")
elif self.type == "fixed":
if self.value <= 0:
raise ValueError("El descuento fijo debe ser positivo")
else:
raise ValueError("Tipo de descuento no válido")
# Validación condicional
if self.type == "percentage" and self.max_amount is not None:
if self.max_amount <= 0:
raise ValueError("El monto máximo debe ser positivo")
return self
Integración con FastAPI
En FastAPI, estos modelos y validaciones se utilizan directamente en las definiciones de rutas:
from fastapi import FastAPI, HTTPException, Path, Query
from pydantic import BaseModel, EmailStr, Field, field_validator
from enum import Enum
from typing import List, Optional
app = FastAPI()
class UserRole(str, Enum):
ADMIN = "admin"
USER = "user"
class UserCreate(BaseModel):
username: str = Field(min_length=3, max_length=50)
email: EmailStr
password: str = Field(min_length=8)
role: UserRole = UserRole.USER
@field_validator('username')
@classmethod
def username_alphanumeric(cls, v: str) -> str:
if not v.isalnum():
raise ValueError('El nombre de usuario debe ser alfanumérico')
return v
class UserResponse(BaseModel):
id: int
username: str
email: EmailStr
role: UserRole
@app.post("/users/", response_model=UserResponse)
async def create_user(user: UserCreate):
# Aquí iría la lógica para crear el usuario en la base de datos
# Simulamos la creación asignando un ID
return UserResponse(
id=1,
username=user.username,
email=user.email,
role=user.role
)
@app.get("/users/{user_id}")
async def get_user(
user_id: int = Path(..., gt=0, description="ID del usuario"),
include_inactive: bool = Query(False, description="Incluir usuarios inactivos")
):
# Aquí iría la lógica para obtener el usuario de la base de datos
if user_id == 999 and not include_inactive:
raise HTTPException(status_code=404, detail="Usuario no encontrado")
return {"id": user_id, "username": "usuario_ejemplo", "active": not include_inactive}
En este ejemplo:
- Usamos
Field
para validar los campos del modeloUserCreate
- Aplicamos un validador personalizado para el nombre de usuario
- Definimos un modelo separado
UserResponse
para la respuesta - Utilizamos
Path
yQuery
para validar parámetros de ruta y consulta
Validación de archivos y contenido binario
Pydantic también puede validar contenido binario y archivos:
from pydantic import BaseModel, field_validator
from fastapi import FastAPI, File, UploadFile
from typing import List
app = FastAPI()
class ImageMetadata(BaseModel):
filename: str
content_type: str
size: int
@field_validator('content_type')
@classmethod
def validate_image_type(cls, v: str) -> str:
allowed_types = ["image/jpeg", "image/png", "image/gif"]
if v not in allowed_types:
raise ValueError(f"Tipo de imagen no permitido. Debe ser uno de: {allowed_types}")
return v
@field_validator('size')
@classmethod
def validate_size(cls, v: int) -> int:
max_size = 5 * 1024 * 1024 # 5MB
if v > max_size:
raise ValueError(f"El archivo es demasiado grande. Máximo: 5MB")
return v
@app.post("/upload/")
async def upload_image(image: UploadFile = File(...)):
# Validamos los metadatos del archivo
metadata = ImageMetadata(
filename=image.filename,
content_type=image.content_type,
size=len(await image.read())
)
# Importante: resetear la posición del archivo después de leerlo
await image.seek(0)
# Procesar el archivo...
return {"filename": metadata.filename, "size": metadata.size}
Este enfoque nos permite validar aspectos como el tipo MIME y el tamaño del archivo antes de procesarlo.
Validación de JSON anidado
Para validar estructuras JSON complejas y anidadas, podemos combinar modelos:
from pydantic import BaseModel, Field
from typing import List, Dict, Any, Optional
class Address(BaseModel):
street: str
city: str
postal_code: str
country: str
class OrderItem(BaseModel):
product_id: int
quantity: int = Field(gt=0)
unit_price: float = Field(gt=0)
@property
def total_price(self) -> float:
return self.quantity * self.unit_price
class Order(BaseModel):
id: int
customer_id: int
items: List[OrderItem] = Field(min_length=1)
shipping_address: Address
billing_address: Optional[Address] = None
metadata: Dict[str, Any] = {}
@model_validator(mode='after')
def set_billing_address(self) -> 'Order':
# Si no se proporciona dirección de facturación, usar la de envío
if self.billing_address is None:
self.billing_address = self.shipping_address
return self
@property
def total_amount(self) -> float:
return sum(item.total_price for item in self.items)
Este modelo puede validar estructuras JSON complejas como:
order_data = {
"id": 12345,
"customer_id": 42,
"items": [
{"product_id": 101, "quantity": 2, "unit_price": 29.99},
{"product_id": 205, "quantity": 1, "unit_price": 59.50}
],
"shipping_address": {
"street": "Calle Gran Vía 123",
"city": "Madrid",
"postal_code": "28013",
"country": "España"
},
"metadata": {
"source": "web",
"promotion_code": "SUMMER2023"
}
}
order = Order.model_validate(order_data)
print(f"Total del pedido: {order.total_amount}€")
print(f"Dirección de facturación: {order.billing_address.city}") # Madrid (copiada de shipping_address)
Ejercicios de esta lección Validación de datos con Pydantic 2
Evalúa tus conocimientos de esta lección Validación de datos con Pydantic 2 con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Todas las lecciones de FastAPI
Accede a todas las lecciones de FastAPI y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Fastapi Y Configuración
Introducción Y Entorno
Respuestas Y Códigos De Estado
Api Rest
Validación De Datos Con Pydantic 2
Api Rest
Rutas Y Parámetros
Api Rest
Conexión De Fastapi Con Sqlalchemy
Persistencia
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender qué es Pydantic y su importancia en la validación de datos.
- Aprender a definir modelos de datos con Pydantic y validar instancias.
- Utilizar tipos básicos, complejos y validaciones personalizadas en modelos.
- Integrar modelos Pydantic con FastAPI para validar entradas y respuestas.
- Aplicar configuraciones avanzadas, campos computados y validaciones condicionales.