Validación avanzada de formularios

Intermedio
Django
Django
Actualizado: 18/04/2026

Niveles de validación en Django

Django aplica la validación en varios niveles de forma secuencial:

Diagrama conceptual de Validación avanzada de formularios

  1. Validación del campo (clean_nombre_campo()): válida el valor de un campo específico.
  2. Validación del formulario (clean()): válida relaciones entre varios campos.
  3. Validación del modelo (si se usa ModelForm): aplica las restricciones del modelo.

Validación de campo individual

El método clean_<nombre_campo>() se llama automáticamente para cada campo y debe devolver el valor limpio o lanzar ValidationError:

from django import forms
from django.core.exceptions import ValidationError
from .models import Usuario

class RegistroForm(forms.ModelForm):
    password = forms.CharField(widget=forms.PasswordInput)
    confirmar_password = forms.CharField(widget=forms.PasswordInput)

    class Meta:
        model = Usuario
        fields = ['username', 'email', 'password']

    def clean_username(self):
        username = self.cleaned_data.get('username')
        if len(username) < 4:
            raise ValidationError('El nombre de usuario debe tener al menos 4 caracteres.')
        if Usuario.objects.filter(username__iexact=username).exists():
            raise ValidationError(f'El nombre de usuario "{username}" ya está en uso.')
        return username.lower()  # Normalizar a minúsculas

    def clean_email(self):
        email = self.cleaned_data.get('email')
        dominio = email.split('@')[1] if '@' in email else ''
        dominios_bloqueados = ['tempmail.com', 'guerrillamail.com', '10minutemail.com']
        if dominio in dominios_bloqueados:
            raise ValidationError('No se permiten correos temporales.')
        return email.lower()

Validación cruzada con clean()

El método clean() recibe todos los campos ya validados individualmente y permite validaciones que involucran múltiples campos:

def clean(self):
    cleaned_data = super().clean()
    password = cleaned_data.get('password')
    confirmar_password = cleaned_data.get('confirmar_password')

    if password and confirmar_password:
        if password != confirmar_password:
            # Error no asociado a un campo específico (aparece en form.non_field_errors())
            raise ValidationError('Las contraseñas no coinciden.')
        if len(password) < 8:
            self.add_error('password', 'La contraseña debe tener al menos 8 caracteres.')

    return cleaned_data

Para asociar el error a un campo específico dentro de clean(), usa self.add_error('nombre_campo', mensaje).

Validators reutilizables

Los validators son funciones o clases reutilizables que se pueden aplicar a múltiples campos y formularios:

from django.core.validators import RegexValidator, MinValueValidator, MaxValueValidator
from django.core.exceptions import ValidationError

# Validador como función
def validar_codigo_postal(valor):
    if not valor.isdigit() or len(valor) != 5:
        raise ValidationError(
            '%(value)s no es un código postal válido. Debe tener 5 dígitos.',
            params={'value': valor}
        )

# Validador como clase (reutilizable con configuración)
class ValidadorEdadMinima:
    def __init__(self, edad_minima):
        self.edad_minima = edad_minima

    def __call__(self, valor):
        if valor < self.edad_minima:
            raise ValidationError(
                f'Debes tener al menos {self.edad_minima} años para registrarte.'
            )

    def deconstruct(self):
        return (
            f'{self.__class__.__module__}.{self.__class__.__name__}',
            [self.edad_minima],
            {}
        )

# RegexValidator integrado
validar_telefono = RegexValidator(
    regex=r'^\+?[1-9]\d{8,14}$',
    message='Número de teléfono no válido. Formato: +34123456789',
    code='telefono_invalido'
)

Aplicar validators en el modelo o en el formulario:

class PerfilForm(forms.ModelForm):
    codigo_postal = forms.CharField(validators=[validar_codigo_postal])
    telefono = forms.CharField(validators=[validar_telefono])
    edad = forms.IntegerField(
        validators=[
            MinValueValidator(18, message='Debes ser mayor de edad.'),
            MaxValueValidator(120, message='Edad no válida.'),
            ValidadorEdadMinima(18)
        ]
    )

Mostrar errores en plantillas

Django proporciona varias formas de renderizar errores en plantillas:

<form method="post">
    {% csrf_token %}

    <!-- Errores globales del formulario (non_field_errors) -->
    {% if form.non_field_errors %}
        <div class="alert alert-danger">
            {% for error in form.non_field_errors %}
                <p>{{ error }}</p>
            {% endfor %}
        </div>
    {% endif %}

    <!-- Renderizado campo por campo con errores -->
    {% for field in form %}
        <div class="campo {% if field.errors %}tiene-error{% endif %}">
            {{ field.label_tag }}
            {{ field }}
            {% if field.errors %}
                <ul class="errores">
                    {% for error in field.errors %}
                        <li>{{ error }}</li>
                    {% endfor %}
                </ul>
            {% endif %}
            {% if field.help_text %}
                <small>{{ field.help_text }}</small>
            {% endif %}
        </div>
    {% endfor %}

    <button type="submit">Registrarse</button>
</form>

Formulario de contacto completo

class FormularioContacto(forms.Form):
    nombre = forms.CharField(
        max_length=100,
        min_length=2,
        error_messages={
            'min_length': 'El nombre debe tener al menos 2 caracteres.',
            'required': 'El nombre es obligatorio.',
        }
    )
    email = forms.EmailField(
        error_messages={'invalid': 'Introduce una dirección de correo válida.'}
    )
    asunto = forms.ChoiceField(
        choices=[
            ('', '--- Selecciona un asunto ---'),
            ('consulta', 'Consulta general'),
            ('soporte', 'Soporte técnico'),
            ('facturacion', 'Facturación'),
        ]
    )
    mensaje = forms.CharField(
        widget=forms.Textarea(attrs={'rows': 5}),
        min_length=20,
        max_length=1000
    )
    acepto_politica = forms.BooleanField(
        error_messages={'required': 'Debes aceptar la política de privacidad.'}
    )

    def clean_asunto(self):
        asunto = self.cleaned_data.get('asunto')
        if not asunto:
            raise ValidationError('Por favor, selecciona un asunto.')
        return asunto

La validación multinivel de Django garantiza que los datos que llegan a cleaned_data cumplen todas las reglas definidas, simplificando la lógica de las vistas al tener la certeza de que solo se procesa cuando form.is_valid() devuelve True.

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, Django 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 Django

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

Aprendizajes de esta lección

Implementar validación de campo individual con métodos clean_campo(). Usar el método clean() para validación cruzada entre campos del formulario. Crear validadores reutilizables con la clase Validator y funciones validadoras. Aplicar RegexValidator y otros validadores integrados de Django. Mostrar errores de validación correctamente en las plantillas.