Validación entre múltiples campos#

Esta es la entrega número 20 de la serie de tutoriales de Django Ninja.
En el artículo anterior terminamos de explicar la validación personalizada de un solo campo; en esta entrega discutiremos la validación entre múltiples campos (cross-field).
La validación entre múltiples campos es también un requerimiento muy común en el desarrollo de API: por ejemplo, al registrar una cuenta, se debe garantizar que los campos «contraseña» y «confirmar contraseña» tengan el mismo contenido; al seleccionar un rango de fechas, la fecha de inicio no puede ser posterior a la fecha de término, etc.
Estos escenarios de validación no se pueden lograr mediante la validación de un solo campo, ya que requieren examinar simultáneamente la relación lógica entre múltiples campos para garantizar la coherencia y corrección de los datos en su conjunto.
Este artículo explicará cómo lograr requerimientos de validación entre múltiples campos a través de Pydantic: tomando «confirmar contraseña» como ejemplo para mostrar la aplicación práctica de esta función.
Todos los cambios de código de este artículo se pueden consultar en este PR.
Proyecto de ejemplo en GitHub#
Validación entre múltiples campos y separación de responsabilidades#
En realidad, ya sea una validación personalizada de un solo campo o entre múltiples campos, no necesariamente tiene que llevarse a cabo mediante Pydantic.
En teoría, la validación de datos se puede realizar directamente en la función view; por ejemplo, obteniendo los valores de los campos ingresados y verificando manualmente su validez. Lo mismo aplica para la validación entre múltiples campos.
Sin embargo, esta es una práctica conveniente pero «tosa» (rústica): solo es adecuada cuando la lógica de validación es extremadamente sencilla.
Realizar la validación de datos a través de Pydantic aporta un beneficio evidente: la separación de responsabilidades (Separation of Concerns).
Separación de responsabilidades#
La separación de responsabilidades (Separation of Concerns) es un principio de diseño. Defiende dividir las responsabilidades de las distintas funciones del programa en módulos o capas independientes.
Cada módulo se enfoca principalmente en una dirección u objetivo específico, evitando así acoplar múltiples funciones distintas en un solo lugar. Esta división permite que el programa sea más fácil de probar, mantener y extender.
Siguiendo la separación de responsabilidades, la lógica de validación de datos debe concentrarse en el Schema, en lugar de realizarse dentro de la función view.
De este modo, la view puede concentrarse en procesar la lógica de negocio central, delegando la validación de datos a componentes dedicados.
A través del mecanismo de validación de Pydantic, podemos lograr la separación de responsabilidades, manteniendo separadas la validación de datos y la lógica de negocio. Esto no solo mejora la estructura del código, sino que también hace que el proceso de desarrollo sea más claro y estable.
Nuevo requerimiento: confirmar contraseña#
Implementaremos una función muy sencilla, pero suficiente para explicar a fondo el valor de la validación entre múltiples campos: confirmar contraseña.
Recordemos primero el contenido del Schema de solicitud de la API «Crear usuario» al finalizar la entrega anterior:
class CreateUserRequest(Schema):
username: str = Field(examples=['Alice'])
email: str = Field(examples=['alice@example.com'])
password: str = Field(min_length=8, examples=['password123'])
bio: str | None = Field(
default=None, examples=['Hello, I am Alice.'])
...
El diseño de este Schema es, claramente, insuficiente.
Porque al registrarse un usuario, la contraseña suele requerir ingresarse dos veces, sirviendo la segunda como «confirmación»: ¡las cosas importantes se dicen dos veces!
Por lo tanto, añadiremos un campo confirm_password y realizaremos una validación entre múltiples campos con password: confirmar que ambos tengan el mismo contenido.
Aunque la lógica de validación sea muy simple, este es precisamente el escenario ideal para la validación entre múltiples campos.
Implementación de validación entre múltiples campos: uso de model_validator#
Pydantic v2 introdujo el decorador @model_validator para manejar la validación entre múltiples campos, el cual es una mejora y reemplazo de @root_validator en Pydantic v1.
El model aquí se refiere al BaseModel de Pydantic (es decir, nuestros Schema), no a los Models de Django.
Reforzaremos la API «Crear usuario» usando @model_validator para añadir la función de «confirmar contraseña».
Veamos directamente el código modificado:
class CreateUserRequest(Schema):
...
password: str = Field(min_length=8, examples=['password123'])
confirm_password: str = Field(
min_length=8, examples=['password123'])
...
@model_validator(mode='after')
def check_passwords_match(self) -> Self:
if self.password != self.confirm_password:
raise ValueError('La contraseña y la confirmación de contraseña deben ser iguales')
return self
Análisis de los puntos clave#
- Se añadió un nuevo campo
confirm_password. - Se utilizó el decorador
@model_validator(mode='after')para definir el método de validación entre campos.- Existen tres opciones para
mode: before, after y wrap. Contiene bastantes detalles y, debido al espacio, no nos extenderemos en este artículo (podría complementarse en un capítulo extra más adelante). - Solo necesitas saber que la mayoría de las veces se usa el modo after; en este caso, el método de validación es un «método de instancia», y el parámetro
selfrepresenta la propia instancia de Schema (inicializada a partir de los datos de entrada).
- Existen tres opciones para
- El método de validación
check_passwords_matchcompara los campospasswordyconfirm_password; si los contenidos no coinciden, lanza unValueError. - Como mencionamos antes, aunque la lógica es muy simple, realmente logra la validación entre dos campos.
- La validación entre múltiples campos se ejecuta solo después de que se hayan completado todas las validaciones de campos individuales.
Aplicación práctica de la separación de responsabilidades#
¡Descubrirás que, en la implementación de esta nueva función de «confirmar contraseña», la función view no cambió en absoluto! Esta es precisamente la encarnación del principio de separación de responsabilidades.
En comparación con implementar la lógica de validación directamente en la función view (lo cual requeriría modificar tanto la view como el Schema), esta forma de implementación es sin duda más limpia y desacoplada.
Respuestas HTTP cuando la validación falla#
Finalmente, echemos un vistazo a qué tipo de respuesta HTTP se obtiene cuando la validación de datos falla.
Los dos primeros puntos ya se mencionaron en el artículo anterior; los volvemos a listar aquí para comparar y repasar.
Violación del límite de longitud de contraseña#
El resultado de la respuesta es el siguiente:
{
"detail": [
{
"type": "string_too_short",
"loc": [
"body",
"payload",
"password"
],
"msg": "String should have at least 8 characters",
"ctx": {
"min_length": 8
}
}
]
}
Esta es la respuesta a «nivel de sistema» proporcionada por Django Ninja al capturar un error de validación de Pydantic, con código de estado 422.
Violación de la regla «debe contener un número»#
La contraseña ingresada no contiene números; el resultado de la respuesta es el siguiente:
{
"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"
}
}
]
}
Parecen muy parecidos, ¿verdad? Así es, porque también se trata del formato de respuesta automático de Django Ninja, salvo que el mensaje de error contiene nuestro contenido personalizado.
Respuesta al lanzar ValueError#
Pero en realidad, esto se debe a que lo que lanzamos en el método de validación fue ValueError, por lo que Django Ninja lo maneja automáticamente por ti.
Una respuesta similar ocurre cuando la confirmación de contraseña no coincide:
{
"detail": [
{
"type": "value_error",
"loc": [
"body",
"payload"
],
"msg": "Value error, La contraseña y la confirmación de contraseña deben ser iguales",
"ctx": {
"error": "La contraseña y la confirmación de contraseña deben ser iguales"
}
}
]
}
Ahora bien, si se lanza otro tipo de error, como el ValidationError de Django o incluso un error definido por nosotros mismos, ¿Django Ninja lo manejará automáticamente?
La respuesta es: no.
Obtendrás un «500 Internal Server Error», lo cual será el tema central de nuestro artículo dentro de dos entregas.
Resumen y siguientes pasos#
En este artículo, presentamos cómo lograr requerimientos de validación entre múltiples campos a través de @model_validator, poniendo en práctica al mismo tiempo el principio de separación de responsabilidades.
Tras aprender estas dos entregas, tu comprensión de la validación de datos en Django Ninja habrá superado a la de la mayoría de las personas.
A continuación, exploraremos a fondo cómo manejar elegantemente los errores y responder adecuadamente cuando la validación de datos falle, mejorando así la experiencia de uso de la API.