Lo que convierte en silencio, la palabra yes que vale y sí que no, y la línea que arregla el fallo de la lección 13.
⏱ 16 min de lectura
Pydantic es lo que hace funcionar a FastAPI, y se puede usar solo: para leer ficheros de configuración, validar mensajes de una cola o dar forma a la respuesta de otra API, que es el bloque A.
Lo que hay que entender de él es qué convierte sin preguntar, porque es generoso y hay conversiones que sorprenden.
n='42' -> 42
n='42.0' -> 42
n=42.0 -> 42
n=42.7 -> ERROR: Input should be a valid integer, got a number
n=True -> 1
b='true' -> True
b='1' -> True
b=1 -> True
b='yes' -> True
b='si' -> ERROR: Input should be a valid boolean
d='2026-01-15' -> date(2026, 1, 15)
d=1768435200 -> date(2026, 1, 15)
d='15/01/2026' -> ERROR: Input should be a valid date or datetime
dec='19.98' -> Decimal('19.98')
dec=19.98 -> Decimal('19.98')
Cuatro cosas de ahí merecen comentario.
42.7 da error y 42.0 no. Es la decisión correcta: convertir cuando no se pierde nada y protestar cuando sí. Es lo contrario de lo que hace int() de Python, que trunca en silencio.
True se convierte en 1. Esta sí es una conversión discutible, y es la que hay que vigilar: si tu campo es un entero y alguien manda true, recibes un uno sin enterarte.
Un número entero se convierte en fecha, interpretándolo como marca de tiempo Unix. Cómodo y significa que un identificador mandado por error en un campo de fecha produce una fecha plausible en lugar de un error.
Y el decimal conserva lo que se ve, incluso viniendo de una coma flotante, porque pydantic convierte pasando por texto. Es la buena noticia de la lección 3: 19.98 no se convierte en 19.979999999999997. Aun así, el dinero se manda como texto, porque lo que llegue ya vendrá roto si el cliente lo mandó como número.
Lo de arriba es el modo laxo, que es el de fábrica. El otro no convierte nada:
python
from pydantic import ConfigDict
classEstricta(BaseModel):
model_config = ConfigDict(strict=True)
n:int
b:bool
Salida en pantalla
n='42' -> ERROR: Input should be a valid integer
n=42 -> ok
b='true' -> ERROR: Input should be a valid boolean
b=True -> ok
Cuál usar depende de por dónde entran los datos:
Con JSON, el laxo tiene sentido, porque JSON no distingue enteros de decimales igual que Python y muchos clientes mandan números como texto. Rechazar "42" en un campo entero produce discusiones que no llevan a nada.
Con datos internos, el estricto. Mensajes entre tus propios servicios, ficheros de configuración, lo que sale de tu base de datos: ahí una conversión inesperada esconde un error real.
Y se puede pedir por campo, que suele ser la respuesta correcta:
Tres niveles, como en DRF, y aquí con un matiz de más:
python
from pydantic import field_validator, model_validator
classReserva(BaseModel):
titulo:str= Field(max_length=10)
desde: date
hasta: date
@field_validator('titulo', mode='before')@classmethod
defquitar_espacios(cls, valor):return valor.strip()ifisinstance(valor,str)else valor
@field_validator('titulo')@classmethod
defsin_articulo(cls, valor):if valor.lower().startswith('el '):raise ValueError('No empieces por artículo.')return valor.title()@model_validator(mode='after')deffechas_coherentes(self):ifself.desde >self.hasta:raise ValueError('«hasta» debe ser posterior a «desde».')returnself
El previo corre antes de convertir el tipo y el otro después, y esa es la diferencia que no existe en DRF. Sirve para casos concretos:
python
@field_validator('etiquetas', mode='before')@classmethod
defadmitir_texto(cls, valor):"""Acepta "a,b,c" además de ["a", "b", "c"]."""ifisinstance(valor,str):return[x.strip()for x in valor.split(',')]return valor
Eso no se puede hacer después, porque para entonces el texto ya habría fallado al no ser una lista.
Y la regla de la lección 13 se mantiene igual:
Salida en pantalla
con el título demasiado largo: ['before']
errores: [(('titulo',), 'String should have at most 10 characters')]
-> el validador del modelo NI SE EJECUTA
con el título correcto: ['before', 'after', 'model']
errores: [((), 'Value error, «hasta» debe ser posterior a «desde».')]