Data integrity is the bedrock of any reliable web application. Whether you are building an enterprise SaaS platform, an e-commerce marketplace, Our site or a content management system, accepting dirty, malformed, or unauthorized data into your database inevitably leads to corrupted business logic, broken UI components, and severe security vulnerabilities.
While Django ships with an impressive collection of built-in field validators—such as EmailValidator, MaxLengthValidator, and RegexValidator—real-world business rules are rarely that generic. What happens when you need to ensure that a corporate registration email belongs to an approved domain list? What if a user’s chosen password must not contain sequential numbers, or a start date must occur before an end date while spanning across multiple model fields?
When standard validation falls short, Django allows you to build Custom Validators.
In this comprehensive guide, we will explore Django’s multi-layered validation architecture, write both function-based and class-based custom validators, implement model-level cross-field checks, and review production best practices for data integrity.
The Django Validation Pipeline
Before diving into custom code, it is vital to understand how validation flows through a Django application. Data validation in Django typically happens at three distinct layers:
- Form / Serializer Layer: Validates incoming user input before it touches the database (handling type conversion, required fields, and format checks).
- Model Field Layer: Validates individual field values when
.clean_fields()or.save()is called on a model instance. - Model Clean Layer (
clean()): Validates business logic across multiple fields simultaneously on a model instance.
Writing robust custom validators ensures that your business rules are enforced consistently, whether data is submitted through a web form, an administrative panel, a REST API, or a management script.
Part 1: Function-Based Custom Validators
The simplest way to write a custom validator in Django is to define a standard Python function that accepts a single value as its argument. If the value violates your business rule, the function should raise a django.core.exceptions.ValidationError.
Example: Banning Disposable Email Domains
Imagine you want to prevent users from registering accounts using temporary, disposable email services (like Mailinator or TempMail).
Python
# validators.py
from django.core.exceptions import ValidationError
def validate_no_disposable_emails(value):
"""
Custom validator that rejects known disposable email domains.
"""
forbidden_domains = ['mailinator.com', 'tempmail.com', '10minutemail.com', 'throwawaymail.com']
# Extract domain from email string
domain = value.split('@')[-1].lower()
if domain in forbidden_domains:
raise ValidationError(
f"Registrations from disposable email domain '{domain}' are not permitted.",
code='disposable_email_restricted'
)
Applying the Validator to a Model Field
Once defined, you can attach your custom function directly to any Django model field via the validators argument:
Python
# models.py
from django.db import models
from .validators import validate_no_disposable_emails
class UserProfile(models.Model):
username = models.CharField(max_length=150, unique=True)
email = models.EmailField(unique=True, validators=[validate_no_disposable_emails])
created_at = models.DateTimeField(auto_now_add=True)
When Django runs validation on this model (such as during form processing or model clean steps), it passes the email value into validate_no_disposable_emails(). If a forbidden domain is detected, Django catches the ValidationError and attaches it to the field error dictionary.
Part 2: Class-Based (Callable) Custom Validators
Function-based validators are fantastic for static rules. However, what if your validation rule requires dynamic parameters—such as specifying additional reading a maximum allowed file size or a dynamic age restriction that can change based on configuration or database settings?
To achieve this, you can write a class-based validator (often called a callable class) by implementing the __call__ magic method.
Example: Parameterized File Size Validator
Python
# validators.py
from django.core.exceptions import ValidationError
from django.utils.deconstruct import deconstructible
@deconstructible
class MaxFileSizeValidator:
"""
A class-based validator that checks if an uploaded file exceeds a maximum size in megabytes.
"""
def __init__(self, max_size_mb=5):
self.max_size_mb = max_size_mb
def __call__(self, file_obj):
max_bytes = self.max_size_mb * 1024 * 1024
if file_obj.size > max_bytes:
raise ValidationError(
f"File size exceeds the maximum limit of {self.max_size_mb}MB. "
f"Current file size: {file_obj.size / (1024 * 1024):.2f}MB.",
code='file_too_large'
)
Note on @deconstructible: Always decorate class-based validators with @deconstructible. This tells Django’s migration engine how to serialize the validator class into migration files when it is applied to model fields.
Applying a Class-Based Validator
Python
# models.py
class Document(models.Model):
title = models.CharField(max_length=200)
# Restrict uploads to a maximum of 2 megabytes
file = models.FileField(upload_to='documents/', validators=[MaxFileSizeValidator(max_size_mb=2)])
Part 3: Model-Level Cross-Field Validation (clean())
Field-level validators only examine a single field in isolation. However, many business rules involve comparing two or more fields against each other—such as ensuring a start_date occurs before an end_date, or verifying that a promotional discount percentage does not exceed 100% when combined with a fixed rebate.
For multi-field validation, you override the model’s clean() method.
Example: Validating Date Ranges
Python
# models.py
from django.core.exceptions import ValidationError
from django.db import models
class Project(models.Model):
name = models.CharField(max_length=200)
start_date = models.DateField()
end_date = models.DateField()
def clean(self):
super().clean()
# Cross-field validation check
if self.start_date and self.end_date:
if self.start_date > self.end_date:
raise ValidationError({
'end_date': "The project end date cannot be earlier than the start date."
})
def save(self, *args, **kwargs):
# Note: Model .save() does NOT call clean() automatically by default.
# You must invoke full_clean() explicitly or handle it in forms/serializers.
self.full_clean()
super().save(*args, **kwargs)
By passing a dictionary to ValidationError (e.g., {'end_date': "..."}), Django maps the error directly to that specific form or serializer field in the UI.
Part 4: Custom Validation in Django REST Framework (DRF)
If you are building APIs with Django REST Framework, model clean() methods are executed automatically when using ModelSerializer, but DRF also provides its own explicit validation hooks for granular control.
1. Field-Level Validation (validate_<field>)
Python
# serializers.py
from rest_framework import serializers
from .models import Project
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = ['id', 'name', 'start_date', 'end_date']
def validate_name(self, value):
"""Custom field-level validator in DRF"""
if "confidential" in value.lower() and not self.context['request'].user.is_staff:
raise serializers.ValidationError("Only staff members can create confidential projects.")
return value
2. Object-Level Validation (validate)
Python
def validate(self, data):
"""Custom object-level multi-field validator in DRF"""
start = data.get('start_date')
end = data.get('end_date')
if start and end and start > end:
raise serializers.ValidationError({
"end_date": "end_date must occur after start_date."
})
return data
Summary Comparison: Validation Approaches
| Validation Approach | Scope | Execution Trigger | Best Used For |
| Function-Based Validators | Single field | Model saving, forms, serializers | Reusable rules (emails, regex, phone formats, URL structures). |
| Class-Based Validators | Single field | Model saving, forms, serializers | Parameterized rules requiring initialization arguments (file sizes, thresholds). |
Model clean() Method | Multi-field / Instance | Forms, admin, or explicit full_clean() calls | Business rules spanning multiple model attributes. |
| DRF Serializer Validation | API Payload | Incoming HTTP requests | API-specific constraints, request context checks, user role rules. |
Production Best Practices
- Always Call
full_clean()on Models: Remember that Django’s default.save()method does not automatically execute modelclean()methods or field-level validators. Always callinstance.full_clean()before saving models manually in business logic, management commands, or custom service layers. - Provide Machine-Readable Error Codes: Always include a clear string
codewhen raisingValidationError(e.g.,code='invalid_domain'). Frontend applications can parse these codes to display localized error messages or trigger specific UI states without relying on fragile English error strings. - Keep Validators Stateless: Validators should be pure, stateless functions or callables. Avoid making heavy database queries or external network requests inside validators if possible, as validation runs frequently across forms, admin panels, and API endpoints.
Conclusion
Custom validators are an essential tool for maintaining pristine data integrity across your Django applications. By decoupling data validation logic from your views and embedding it securely inside reusable functions, class callables, model clean() methods, click to read more and DRF serializers, you ensure that your database remains a trusted, reliable source of truth.