Django model validation is one of the most important topics for building reliable, professional, and maintainable applications. At first, many Django beginners focus mainly on forms and views, because that is where users enter data and where requests are handled. That is normal. But as projects grow, developers quickly realize that data integrity cannot depend only on form-level checks. Data may come from many different places: Django admin, custom forms, API endpoints, management commands, scripts, fixtures, imported CSV files, background tasks, or even shell operations. If the rules that define what counts as “valid data” are not centralized, then invalid or inconsistent records can enter the database through paths that bypass forms entirely. This is exactly why model validation matters. It places validation logic close to the model itself, which is the part of the application that defines the structure and meaning of your data.
A model in Django represents far more than a database table. It represents a real concept in your application such as an article, order, product, enrollment, invoice, or booking. Each of these concepts usually has business rules. For example, a product price should not be negative, an event’s end date should not come before its start date, a user should not register twice for the same course, a published article should have a title and content, and a discount should not exceed the original price. Some of these rules can be enforced at the database level, some at the form level, and some through model validation. Understanding where each kind of validation belongs is a key part of advanced Django development.
To begin clearly, it helps to distinguish between three related but different ideas: database constraints, form validation, and model validation. Database constraints are rules enforced directly by the database, such as NOT NULL, UNIQUE, or check constraints. These are very strong because they protect data regardless of how it reaches the database. Form validation is performed when a user submits a form and is useful for providing friendly feedback before saving. Model validation sits in between: it is Python-level validation attached to the model layer. It helps centralize business logic in one place and can be reused across forms, admin, and manual save workflows when called properly. A professional Django application often combines all three layers.
Let us start with a simple model:
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.PositiveIntegerField(default=0)
def __str__(self):
return self.nameAt first glance, this model looks fine. The price field allows decimal numbers, and stock is a positive integer, which already gives some structural safety. But imagine that your business rule says a product price must be greater than zero, not just any decimal number. Also imagine that the product name should not be too short. Those are model-level business rules that we may want to validate.
Django provides model validation mainly through the full_clean() method, which runs several layers of checks. When called, full_clean() performs field validation, model-wide validation, uniqueness checks, and constraint checks. Internally, it calls methods such as clean_fields(), clean(), validate_unique(), and constraint-related validation. The most important custom hook for developers is usually clean(), because that is where you place custom model-wide validation logic.
Here is an example:
from django.core.exceptions import ValidationError
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.PositiveIntegerField(default=0)
def clean(self):
if self.price <= 0:
raise ValidationError({
'price': 'Price must be greater than zero.'
})
if len(self.name.strip()) < 3:
raise ValidationError({
'name': 'Product name must contain at least 3 characters.'
})
def __str__(self):
return self.nameThis is your first major model validation example. The clean() method is where you define custom validation rules that apply to the model instance as a whole. If the price is zero or negative, validation fails. If the name is too short, validation fails. Notice that ValidationError can be raised with a dictionary mapping field names to error messages. This is useful because forms and admin interfaces can attach those messages directly to the relevant fields.
An essential point must be understood here: Django does not automatically call full_clean() when you call save() on a model instance. This surprises many beginners. They assume that because they defined a clean() method, it will always run before saving, but that is not the default behavior. If you create a model instance manually and call save(), Django will save it unless the database itself blocks it. For example:
product = Product(name='TV', price=-100, stock=5)
product.save()This will save successfully unless you explicitly call full_clean() first or add some other safeguard. The correct pattern is:
product = Product(name='TV', price=-100, stock=5)
product.full_clean()
product.save()If validation fails, full_clean() raises ValidationError and the object will not be saved unless you handle or ignore the exception. This is one of the most important lessons in Django model validation: defining clean() is not enough; you must also ensure that validation is actually triggered in your workflows.
Forms and ModelForms handle this nicely because they call model validation during form validation. That means if a user submits a Django form backed by a model, your clean() logic usually gets executed automatically as part of form.is_valid(). This is why many beginners think model validation is always automatic. They see it working in forms, but they do not realize that direct save() calls bypass it unless full_clean() is explicitly used.
Now let us go deeper into field-level validation versus model-level validation. If a rule concerns one field independently, it may be better to use a validator directly on the field. Django supports reusable field validators through the validators argument. For example:
from django.core.validators import MinValueValidator
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=200)
price = models.DecimalField(
max_digits=10,
decimal_places=2,
validators=[MinValueValidator(0.01)]
)
stock = models.PositiveIntegerField(default=0)This approach is clean because it attaches a simple rule directly to the field. If the rule is only about that one field and does not depend on other fields, field validators are often more reusable and elegant than putting everything inside clean(). For example, minimum price, maximum length, regex validation, email format, URL rules, and numeric ranges often fit naturally at the field level.
However, many important business rules involve relationships between multiple fields. That is where clean() becomes essential. Suppose you have an event model:
from django.core.exceptions import ValidationError
from django.db import models
class Event(models.Model):
title = models.CharField(max_length=200)
start_date = models.DateField()
end_date = models.DateField()
is_online = models.BooleanField(default=False)
location = models.CharField(max_length=255, blank=True)
def clean(self):
if self.end_date < self.start_date:
raise ValidationError({
'end_date': 'End date cannot be earlier than start date.'
})
if not self.is_online and not self.location.strip():
raise ValidationError({
'location': 'Location is required for physical events.'
})
def __str__(self):
return self.titleThis is a classic model validation scenario. One rule compares two fields: end_date must not come before start_date. Another rule says that if the event is not online, a location is required. These cannot be expressed easily as isolated field validators, because they depend on the combination of values. Model-level validation is the correct place for such logic.
Let us look more carefully at how ValidationError can be raised. You can raise it in different forms depending on the type of error. Raising a dictionary is best when the error belongs to specific fields:
raise ValidationError({
'price': 'Price must be greater than zero.'
})You can also raise a non-field error that applies to the model more generally:
raise ValidationError('This model contains invalid data.')Or:
raise ValidationError({
'__all__': 'Start date and end date combination is invalid.'
})In forms, __all__ becomes a non-field error. This is useful when the issue concerns the whole object rather than one field specifically.
Another very important part of model validation is uniqueness. Django handles field uniqueness through unique=True and multi-field uniqueness through UniqueConstraint or the older unique_together. For example:
class Enrollment(models.Model):
student = models.ForeignKey('Student', on_delete=models.CASCADE)
course = models.ForeignKey('Course', on_delete=models.CASCADE)
enrolled_at = models.DateTimeField(auto_now_add=True)
class Meta:
constraints = [
models.UniqueConstraint(fields=['student', 'course'], name='unique_student_course')
]This ensures that a student cannot enroll in the same course twice. When full_clean() runs, Django checks uniqueness rules as well. But here it is important to understand the role of the database. Uniqueness rules are strongest when enforced as database constraints, because they protect against race conditions and all entry paths. If two requests try to create the same enrollment at the same time, only the database can truly guarantee consistency. So while model validation is important, uniqueness and integrity rules should often also exist at the database level.
Now let us talk about clean_fields(). Django uses this internally during full_clean(), and it validates each field according to field definitions and validators. For example, required fields, max length, choices, email format, and custom validators are checked there. Normally, you do not override clean_fields() unless you have a special reason. Most custom logic goes into clean(). But it is useful to know that field-level and model-level validation are separate steps within full_clean().
Another useful feature is custom validators as reusable classes or functions. Suppose you want to ensure a title does not contain certain forbidden words:
from django.core.exceptions import ValidationError
def validate_forbidden_words(value):
forbidden_words = ['spam', 'fake', 'scam']
for word in forbidden_words:
if word in value.lower():
raise ValidationError(f'The word "{word}" is not allowed in the title.')Then:
class Article(models.Model):
title = models.CharField(max_length=200, validators=[validate_forbidden_words])
content = models.TextField()
published = models.BooleanField(default=False)This is a strong pattern because the validator is reusable and can be attached to any field where needed. If your validation logic applies to a single field and might be reused elsewhere, a validator function is often better than embedding it in clean().
Django also supports class-based validators. These are useful when validation needs parameters or reusable structure. But even with simple functions, you can already build powerful reusable field-level validation systems.
Now let us examine a more realistic blog example:
from django.core.exceptions import ValidationError
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
content = models.TextField()
published = models.BooleanField(default=False)
published_at = models.DateTimeField(blank=True, null=True)
def clean(self):
if self.published and not self.content.strip():
raise ValidationError({
'content': 'Published articles must have content.'
})
if self.published and self.published_at is None:
raise ValidationError({
'published_at': 'Published articles must have a publication date.'
})
def __str__(self):
return self.titleHere, the logic reflects business meaning: drafts can be incomplete, but published articles must have content and a publication date. This kind of rule belongs naturally in model validation because it describes the valid state of the model itself.
Sometimes developers ask whether they should override save() to call full_clean() automatically. For example:
class Product(models.Model):
name = models.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2)
def clean(self):
if self.price <= 0:
raise ValidationError({'price': 'Price must be greater than zero.'})
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)This guarantees that every normal save() call validates first. It seems attractive, and in some projects it is a useful approach. But it also comes with trade-offs. For example, bulk_create() and update() do not call save(), so validation is still bypassed there. Also, automatic validation in save() can sometimes surprise code that expects to save partial objects during intermediate steps. Whether to do this depends on project style and team conventions. Some teams prefer explicit full_clean() calls in service layers or forms. Others prefer stricter models that validate on every save. There is no single universal rule, but you should be aware of the implications.
It is also important to remember that bulk operations bypass validation. For example:
Product.objects.bulk_create([
Product(name='A', price=-10),
Product(name='B', price=20),
])This will not automatically run full_clean() for each object. The same is true for update() and similar direct database operations. This means model validation is not a magical firewall around the database. It is a Python-level mechanism that helps when used properly, but database constraints are still essential for critical integrity rules.
A very useful validation pattern is combining field validators, model clean(), and database constraints together. For example, consider a discount model:
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models
class Discount(models.Model):
name = models.CharField(max_length=100)
original_price = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(0.01)])
discount_price = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(0.01)])
def clean(self):
if self.discount_price >= self.original_price:
raise ValidationError({
'discount_price': 'Discount price must be lower than original price.'
})
def __str__(self):
return self.nameHere, field validators ensure both prices are positive. Model validation ensures the discount price is actually lower than the original price. This layered approach is one of the best ways to build robust applications.
Django model validation also works very well with the admin. If a model is edited in Django admin using a standard ModelForm, your clean() method will be executed as part of form validation. That means validation errors can appear directly in the admin UI, helping administrators avoid saving invalid data. This is another reason to keep important business validation close to the model instead of scattering it across individual forms.
Now let us discuss ModelForm interaction more explicitly. Suppose you have:
from django import forms
from .models import Product
class ProductForm(forms.ModelForm):
class Meta:
model = Product
fields = ['name', 'price', 'stock']When you call:
form = ProductForm(request.POST)
if form.is_valid():
form.save()Django validates both form-level rules and model-level rules, including your model’s clean() method. This is why model validation is especially powerful: it becomes reusable automatically in forms. If tomorrow you create another form for the same model, the validation logic is already there.
Another advanced validation tool in Django is database-level CheckConstraint. For example:
class Product(models.Model):
name = models.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2)
class Meta:
constraints = [
models.CheckConstraint(check=models.Q(price__gt=0), name='price_gt_zero')
]This pushes the rule into the database: price must be greater than zero. This is stronger than Python validation alone. Ideally, highly important integrity rules such as numeric positivity, uniqueness, and forbidden combinations should often be backed by database constraints when possible. Model validation remains useful for friendly error handling and more complex business logic, but constraints protect the data at the lowest level.
You may wonder how to choose the right validation layer. A practical rule is this:
- Use field validators for simple single-field rules.
- Use
clean()for rules involving multiple fields or business meaning. - Use database constraints for critical integrity rules that must never be violated.
- Use form validation for presentation-specific or user-input-specific checks and messages.
In strong applications, these layers complement each other rather than compete.
Let us take a reservation example:
from django.core.exceptions import ValidationError
from django.db import models
class Reservation(models.Model):
guest_name = models.CharField(max_length=150)
room_number = models.PositiveIntegerField()
check_in = models.DateField()
check_out = models.DateField()
def clean(self):
if self.check_out <= self.check_in:
raise ValidationError({
'check_out': 'Check-out date must be later than check-in date.'
})
def __str__(self):
return f"{self.guest_name} - Room {self.room_number}"This rule is easy to understand and strongly connected to the meaning of a reservation. It makes the model safer everywhere it is used. This is the real value of model validation: it captures business truth close to the data model.
Testing model validation is also very important. Because validation rules protect data integrity, they should have clear tests. For example:
from django.core.exceptions import ValidationError
from django.test import TestCase
from .models import Product
class ProductValidationTests(TestCase):
def test_price_must_be_positive(self):
product = Product(name='Laptop', price=-100, stock=5)
with self.assertRaises(ValidationError):
product.full_clean()This kind of test ensures your validation behaves as expected and remains stable as the project evolves. You can also test specific error messages, field associations, and combined rules.
Another subtle point is that clean() should usually not have side effects. Its role is to validate, not to mutate unrelated data or trigger external actions. Validation code should remain focused on checking correctness. If you start sending emails, creating related objects, or changing system state inside clean(), the code becomes harder to understand and maintain. Keep validation about validation.
A good practice inside clean() is to accumulate multiple errors before raising them when possible, especially if several fields may be invalid at the same time. For example:
def clean(self):
errors = {}
if self.price <= 0:
errors['price'] = 'Price must be greater than zero.'
if len(self.name.strip()) < 3:
errors['name'] = 'Name must contain at least 3 characters.'
if errors:
raise ValidationError(errors)This is better than raising immediately on the first problem, because it allows the user to see all validation issues at once.
In conclusion, Django model validation is a fundamental tool for protecting data integrity and expressing business rules clearly at the model layer. It helps ensure that objects are valid not only when entered through forms, but also when created through admin, scripts, imports, APIs, or other code paths—provided validation is actually called. Through clean(), field validators, full_clean(), uniqueness checks, and database constraints, Django offers a flexible and layered validation system. The key ideas to remember are that validation should live as close as possible to the data meaning, that save() does not automatically call full_clean(), and that strong applications usually combine Python validation with database constraints for maximum reliability. Once you understand model validation well, your Django applications become more trustworthy, easier to maintain, and more professional in how they handle data.
What you learned in this tutorial
In this tutorial, you learned what Django model validation is, why it matters, how full_clean() works, how to use the clean() method, when to use field validators versus model validation, how uniqueness and constraints interact with validation, why save() does not automatically validate, how forms and admin reuse model validation, how bulk operations bypass it, and how to test validation rules effectively.
💬 Comments
No comments yet. Be the first to comment!
Login to comment.