Validación de un solo campo#

Esta es la entrega número 19 de la serie de tutoriales de Django Ninja. Entramos al capítulo 5: Validación de datos y manejo de errores.
La validación de datos es uno de los requerimientos clave en el desarrollo de API. Se encarga de garantizar que los datos enviados desde el cliente cumplan con lo esperado, evitando así errores potenciales y problemas de seguridad.
Una validación de datos eficaz permite que la API entregue una respuesta inmediata y amigable cuando recibe datos erróneos, elevando la estabilidad del sistema y la experiencia de usuario.
En Django Ninja, la herramienta central para la validación de datos es Pydantic. Ofrece potentes funciones de validación que no solo comprueban los tipos de datos, sino que también permiten implementar fácilmente validaciones personalizadas.
Este artículo explicará cómo implementar la validación personalizada de un solo campo usando Pydantic en Django Ninja; la siguiente entrega tratará sobre la validación personalizada entre múltiples campos (cross-field).
Todos los cambios de código de este artículo se pueden consultar en este PR.
Proyecto de ejemplo en GitHub#
Visión general del capítulo 5#
La validación de datos es sumamente importante, y cuando falla, el programa suele lanzar un error de validación. Cómo manejar eficazmente estos errores es el ámbito que aborda el «manejo de errores».
Este capítulo explorará estos dos temas estrechamente relacionados a lo largo de un total de 4 artículos:
- Vol. 19: Validación de datos (Parte 1) Validación de un solo campo en Pydantic (este artículo)
- Vol. 20: Validación de datos (Parte 2) Validación entre múltiples campos en Pydantic
- Vol. 21: Manejo de errores (Parte 1) HttpError y respuestas HTTP personalizadas
- Vol. 22: Manejo de errores (Parte 2) Manejo global de errores: Uso de Exception Handlers
En los dos primeros artículos, aprenderemos cómo lograr una validación de datos flexible para garantizar que los datos de entrada cumplan con lo esperado y lanzar errores cuando sea necesario.
En los dos últimos artículos, discutiremos cómo manejar los diversos errores que pueden surgir durante el flujo de la API (sin limitarse a errores de validación) para brindar una mejor experiencia de usuario.
Los mecanismos de validación de datos y manejo de errores de Django Ninja son más complejos en comparación con Django REST framework, por lo que debemos dedicarles entregas completas para ayudarte a comprenderlos con claridad.
Corrección de la API#
Tomaremos como ejemplo la API recién creada en el artículo anterior: Crear usuario.
Seguiremos mejorándola, añadiendo validaciones personalizadas para hacer más confiables los datos enviados por el cliente.
Sin embargo, primero debo hacer unas correcciones de errores; el código corregido es el siguiente:
@router.post('/users/', summary='Crear usuario', response={201: dict})
def create_user(...) -> tuple[int, dict]:
"""
Crear usuario
"""
user = User(
username=payload.username,
email=payload.email,
bio=payload.bio,
)
# Usar el método set_password para encriptar la contraseña
user.set_password(raw_password=payload.password)
user.save()
return 201, {'id': user.id, 'username': user.username}
Principalmente se corrigieron estos dos puntos:
- Se añadió el parámetro
response={201: dict}al decoradorrouter. Originalmente no estaba definido, y al usar esta API en la práctica provocaría un error. Esto se debe a que por defecto solo hay respuesta 200; si se desea una respuesta distinta de 200, debe declararse a través del parámetroresponse. - Se utilizó el método
set_passwordpara encriptar la contraseña ingresada por el usuario. Es una función integrada de Django para evitar almacenar contraseñas directamente en la base de datos. No almacenar contraseñas en texto plano es, sin duda, el ABC del desarrollo moderno.
Terminadas las correcciones, entramos oficialmente en tema.
Diferentes «niveles» de validación#
Al tratarse de validación, por supuesto está relacionada principalmente con las solicitudes provenientes del cliente: validar el contenido de la solicitud.
En Django Ninja, cada API puede describir la estructura de datos que recibe definiendo un Schema. Estos Schema se basan en Pydantic y pueden validar automáticamente los datos de la solicitud.
Los type hints dentro del Schema pueden validar los tipos de datos; esta es la validación más básica.
El Pydantic Field mencionado en la entrega anterior puede validar características como la longitud y el rango de los datos. Esta parte se demostrará más adelante.
Todas estas son validaciones de carácter «formal», mientras que este artículo se enfocará en las «validaciones personalizadas» más complejas, basadas en reglas específicas.
Estado actual del Schema de la API de ejemplo#
Tomando como ejemplo «Crear usuario», el cuerpo de la solicitud (request body) recibe campos como username, email, password y bio. A través del Schema que definimos, podemos completar la validación de tipos de datos más básica.
Como se mencionó en el artículo anterior, solo el campo bio es opcional, mientras que los demás son obligatorios (si faltan, se obtendrá una respuesta 422). Por lo tanto, el Schema también valida la «existencia» de los datos.
¡Por ahora se ve bastante bien! Pero no nos conformaremos con eso.
Nuevos requerimientos: reglas de contraseña#
Requerimos que los usuarios cumplan con las siguientes dos reglas al configurar su contraseña:
- La longitud de la contraseña debe ser de al menos 8 caracteres.
- Debe contener al menos un número.
Estas reglas ayudan a mejorar la seguridad de la cuenta, evitando que los usuarios configuren contraseñas demasiado simples.
Teniendo en cuenta el propósito didáctico, no hice las reglas demasiado complejas. Ambas reglas tienen un significado pedagógico específico:
- El límite de longitud mínima se puede implementar directamente mediante Pydantic Field, sin tener que programarlo uno mismo.
- La segunda regla es el plato fuerte: usaremos el decorador
@field_validatorde Pydantic para definir por nuestra cuenta la regla de validación del campo.
Implementación de la validación de reglas de contraseña: uso de field_validator#
Según los requerimientos, podemos usar primero el Field de Pydantic para configurar el límite de longitud mínima:
Como se muestra arriba, solo necesitamos añadir el parámetro min_length=8.
En cuanto a la validación de «debe contener números», se implementará con el decorador @field_validator.
Decorador field_validator#
En Pydantic v1, el nombre de este decorador era validator; fue en v2 donde cambió a field_validator.
Pydantic tiene muchos cambios rompedores de v1 a v2; por ejemplo, que el parámetro example mencionado anteriormente haya cambiado a examples es un caso. Vale la pena prestar atención a esto.
A continuación se muestra el Schema modificado; nos enfocaremos únicamente en la parte de field_validator:
class CreateUserRequest(Schema):
...
password: str
...
@field_validator('password')
@classmethod
def validate_password_contains_number(cls, v: str) -> str:
"""
Validar que la contraseña contenga al menos un número
"""
if not re.search(r'\d', v):
raise ValueError('La contraseña debe contener al menos un número')
return v
Análisis de los puntos clave#
- El decorador
field_validatordebe usar parámetros; el valor válido es el nombre del campo, comopassword. - Aunque no se demuestra en el ejemplo, se puede aplicar a múltiples campos.
- Se escribe
@field_validator('campo1', 'campo2', ...); incluso puedes escribir directamente@field_validator('*')para aplicarlo a todos los campos. - Pero ten en cuenta que estos campos ejecutarán la misma lógica de validación, por lo que en teoría deben ser campos con lógica similar.
- Se escribe
- El nombre del método de validación se puede personalizar; puedes nombrarlo como quieras siempre que para ti resulte comprensible.
- Porque Pydantic se fija principalmente en los nombres de campo especificados en el decorador.
- Esto es muy diferente al patrón de nombres
validate_<nombre_campo>usado en los métodos de validación de Django REST framework.
- La convención de nombre para el parámetro en los métodos de validación de Pydantic es
v, mientras que en Django REST framework esvalue. - Segunda convención: el método de validación devuelve intacto el valor de entrada al tener éxito; al fallar, lanza un error.
- El método de validación de Pydantic es un «método de clase», por lo que el primer parámetro es
cls. De manera especial, puedes omitir el decorador@classmethodporque Pydantic ya lo procesa internamente.- Sin embargo, la documentación oficial sigue recomendando usar
@classmethod, por lo que seguiremos la sugerencia. - Si declaras el decorador
@classmethod, su posición debe ser la más cercana al método de validación.
- Sin embargo, la documentación oficial sigue recomendando usar
¿Quién lo hubiera pensado? ¡Tan pocas líneas esconden tantos detalles interesantes!
Pruebas reales#
Probando la situación donde la longitud de la contraseña es insuficiente, el resultado es:
{
"detail": [
{
"type": "string_too_short",
"loc": [
"body",
"payload",
"password"
],
"msg": "String should have at least 8 characters",
"ctx": {
"min_length": 8
}
},
{
"type": "string_too_short",
"loc": [
"body",
"payload",
"confirm_password"
],
"msg": "String should have at least 8 characters",
"ctx": {
"min_length": 8
}
}
]
}
A continuación, probemos el caso en que la contraseña no contiene números:
{
"detail": [
{
"type": "value_error",
"loc": [
"body",
"payload",
"password"
],
"msg": "Value error, La contraseña debe contener al menos un número",
"ctx": {
"error": "La contraseña debe contener al menos un número"
}
}
]
}
La personalización de las respuestas de error puede ser aún más flexible, aunque ese será el tema del artículo subsecuente «Manejo de errores (Parte 1) HttpError y respuestas HTTP personalizadas», el cual discutiremos en detalle en su momento.
Resumen#
En esta entrega, aprendimos cómo realizar la validación de datos para un solo campo a través de Pydantic, implementando reglas de verificación de fortaleza de contraseña.
En el siguiente artículo, continuaremos con este tema para implementar una validación entre múltiples campos (cross-field) más compleja.