Llamar a obj.save() después de definir validators y un método clean() en el modelo da la impresión de que la validación está garantizada. No lo está. Django no llama a full_clean() durante un save(), y este comportamiento es deliberado. Entender por qué cambia la forma de diseñar la validación en un proyecto.
Lo que save() hace realmente
El ciclo de vida de un save() es más corto de lo que parece:
- Señal
pre_saveenviada field.pre_save()llamado en cada campo (auto_now,auto_now_add, etc.)- SQL
INSERToUPDATEsegún la presencia de unpk - Señal
post_saveenviada
No hay ninguna validación en esta secuencia. Sin comprobación de blank, sin max_length, sin llamada a clean(). Lo mismo aplica a Model.objects.create() y Model.objects.bulk_create(): los tres métodos persisten sin validar. (bulk_create() se trata en detalle en Django in_bulk() y bulk_create() si trabajas con inserciones masivas.)
class Article(models.Model):
title = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
def clean(self):
if "spam" in self.title.lower():
raise ValidationError("Título inválido.")
# Las tres llamadas pasan sin error, incluso con un título de 500 caracteres
Article(title="x" * 500, slug="test").save()
Article.objects.create(title="x" * 500, slug="test2")
Article.objects.bulk_create([Article(title="x" * 500, slug="test3")])
full_clean(): los cuatro pasos
full_clean() es el método a llamar explícitamente para activar toda la cadena de validación de un modelo. Se ejecuta en cuatro pasos en este orden:
1. clean_fields() valida cada campo individualmente: conversión al tipo Python (to_python()), comprobación de blank, null, max_length, choices, y los validators definidos en el campo.
2. clean() es el método de validación entre campos, diseñado para ser sobreescrito en el modelo. Aquí se expresan las reglas que dependen de múltiples campos a la vez.
3. validate_unique() comprueba las restricciones de unicidad: campos unique=True, unique_together, y declaraciones UniqueConstraint sin condición.
4. validate_constraints() comprueba las Meta.constraints más complejas (CheckConstraint, UniqueConstraint condicional, etc.).
from django.core.exceptions import ValidationError
class Event(models.Model):
name = models.CharField(max_length=100)
start_date = models.DateField()
end_date = models.DateField()
def clean(self):
if self.end_date and self.start_date and self.end_date < self.start_date:
raise ValidationError("La fecha de fin debe ser posterior a la de inicio.")
event = Event(name="Conf", start_date="2026-06-10", end_date="2026-06-01")
event.full_clean() # Lanza ValidationError desde clean()
event.save() # Nunca se alcanza
Los errores generados por full_clean() son instancias de ValidationError. Si varios campos tienen errores, todos se recopilan antes de lanzarse juntos en un único diccionario.
form.is_valid(): dos comportamientos según el tipo de formulario
BaseForm: pipeline propio, sin el modelo
Un formulario estándar (forms.Form) tiene su propio pipeline de validación, independiente de cualquier modelo Django:
- Para cada campo:
field.clean()que encadena conversión de tipo, validación de tipo y validators del campo clean_<fieldname>()si el método existe en el formularioclean()del formulario para la validación entre campos
El modelo Django no está involucrado.
ModelForm: llama a full_clean() del modelo
Aquí está lo sorprendente. Un ModelForm hace más que un Form. Después de validar sus propios campos, llama a instance.full_clean(exclude=..., validate_unique=False) dentro de su método _post_clean().
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ["title", "slug"]
form = ArticleForm(data={"title": "spam content", "slug": "test"})
if form.is_valid():
# _post_clean() ha llamado a article_instance.full_clean()
# por lo tanto el clean() del modelo se ha ejecutado
form.save()
En la práctica, si el modelo define un clean() que lanza una ValidationError, ese error se captura y se integra en los errores del formulario. El parámetro validate_unique=False en la llamada existe porque el ModelForm gestiona la unicidad de forma independiente a través de su propio método validate_unique().
Los validators definidos en los campos del modelo también se copian automáticamente a los campos correspondientes del ModelForm.
serializer.is_valid() en DRF: pipeline propio, sin full_clean()
Un ModelSerializer tiene un pipeline de validación distinto, más cercano a un Form estándar que a un ModelForm:
- Para cada campo:
field.to_internal_value()y luegofield.run_validators() - Validators a nivel del serializer (
UniqueValidator,UniqueTogetherValidator, añadidos automáticamente) validate_<fieldname>()si el método existe en el serializervalidate()para la validación entre campos
Lo que no se llama: el clean() del modelo. Incluso para un ModelSerializer, el método clean() definido en el modelo nunca se activa mediante is_valid().
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ["title", "slug"]
serializer = ArticleSerializer(data={"title": "spam content", "slug": "test"})
serializer.is_valid() # True — el clean() del modelo no se llama
serializer.save() # Persiste sin ejecutar la lógica de clean()
Para forzar la ejecución de full_clean() en un serializer, hay que hacerlo explícitamente dentro de validate():
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ["title", "slug"]
def validate(self, attrs):
instance = Article(**attrs)
instance.full_clean()
return attrs
Tabla comparativa
blank / max_length | validators de campo | clean() del modelo | restricciones unique | |
|---|---|---|---|---|
obj.save() | ❌ | ❌ | ❌ | ❌ |
objects.create() | ❌ | ❌ | ❌ | ❌ |
bulk_create() | ❌ | ❌ | ❌ | ❌ |
full_clean() | ✅ | ✅ | ✅ | ✅ |
Form.is_valid() | ✅ | ✅ | ❌ | ❌ |
ModelForm.is_valid() | ✅ | ✅ | ✅ | ✅ |
ModelSerializer.is_valid() | ✅ | ✅ | ❌ | ✅* |
*mediante UniqueValidator añadido automáticamente por el ModelSerializer.
Garantizar la validación en todos los contextos
La solución más directa para no olvidar full_clean() es integrarlo en el save() del modelo mediante un mixin abstracto:
class ValidatedModel(models.Model):
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)
class Meta:
abstract = True
class Article(ValidatedModel):
title = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
Este mixin tiene un coste: full_clean() envía una consulta adicional para verificar validate_unique(). En contextos de importación masiva o scripts de migración, se puede pasar full_clean(validate_unique=False) para reducir este coste, o saltar el mixin llamando directamente a super().save().
La separación validación / persistencia es intencional
Django separa deliberadamente validación y persistencia. Esta separación permite usar el ORM en contextos donde la validación completa no es deseable: scripts de migración de datos, fixtures, importaciones desde fuentes ya validadas. Forzar full_clean() en todos los save() haría que estos casos fueran difíciles de gestionar.
El contrato es explícito: llamar a full_clean() antes de save() en la lógica de negocio, usar un ModelForm para las interfaces de usuario ya que llama a full_clean() automáticamente, y en una API DRF, elegir conscientemente si la lógica del clean() del modelo debe activarse a través del método validate() del serializer.
