$ cat /posts/django-model-validation-custom-validators-and-clean-methods.md

Django Model Validation: Custom Validators and clean() Methods

drwxr-xr-x2026-01-225 min0 views
Django Model Validation: Custom Validators and clean() Methods

Django model validation ensures data integrity by checking field values before saving to the database using built-in validators, custom validators, and clean methods. Validation prevents invalid data from entering the database enforcing business rules, data formats, and constraints beyond basic field types. Understanding validation enables building robust applications with proper error handling, user feedback, and data quality assurance. Django provides multiple validation layers including field-level validators, model-level clean methods, and form validation supporting comprehensive data validation from simple format checks to complex cross-field business rules ensuring data consistency and application reliability across user input, API requests, and data imports.

Built-in Validators

Django provides built-in validators for common validation scenarios including email validation, URL validation, integer ranges, string patterns, and file validation. Built-in validators handle standard validation requirements reducing custom code and ensuring consistent validation across applications. Understanding built-in validators enables quick implementation of common validation rules.

pythonbuiltin_validators.py
# Built-in Django Validators
from django.db import models
from django.core.validators import (
    MinValueValidator, MaxValueValidator,
    MinLengthValidator, MaxLengthValidator,
    EmailValidator, URLValidator,
    RegexValidator, FileExtensionValidator,
    validate_email, validate_slug
)

class Product(models.Model):
    name = models.CharField(
        max_length=200,
        validators=[MinLengthValidator(3)]
    )
    
    # Numeric validation
    price = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        validators=[
            MinValueValidator(0.01),
            MaxValueValidator(999999.99)
        ]
    )
    
    quantity = models.IntegerField(
        validators=[
            MinValueValidator(0),
            MaxValueValidator(10000)
        ]
    )
    
    # String validators
    email = models.EmailField()  # Has built-in email validator
    
    website = models.URLField()  # Has built-in URL validator
    
    slug = models.SlugField()  # Validates slug format
    
    # Phone number with regex
    phone_regex = RegexValidator(
        regex=r'^\+?1?\d{9,15}$',
        message="Phone number must be entered in format: '+999999999'. Up to 15 digits."
    )
    phone_number = models.CharField(
        validators=[phone_regex],
        max_length=17
    )
    
    # Postal code validation
    postal_code = models.CharField(
        max_length=10,
        validators=[
            RegexValidator(
                regex=r'^\d{5}(-\d{4})?$',
                message='Enter a valid US postal code'
            )
        ]
    )
    
    # File validation
    document = models.FileField(
        upload_to='documents/',
        validators=[
            FileExtensionValidator(
                allowed_extensions=['pdf', 'doc', 'docx']
            )
        ]
    )
    
    # Multiple validators
    username = models.CharField(
        max_length=30,
        validators=[
            MinLengthValidator(3),
            RegexValidator(
                regex=r'^[a-zA-Z0-9_]+$',
                message='Username can only contain letters, numbers, and underscores'
            )
        ]
    )

# Using validators directly
def validate_email_example(email):
    from django.core.validators import validate_email
    from django.core.exceptions import ValidationError
    
    try:
        validate_email(email)
        print("Valid email")
    except ValidationError:
        print("Invalid email")

Custom Validators

Custom validators implement application-specific validation rules as reusable functions that raise ValidationError for invalid data. Custom validators enable business logic validation beyond built-in validators supporting domain-specific requirements. Understanding custom validators enables building comprehensive validation supporting complex business rules and data constraints.

pythoncustom_validators.py
# Custom Validators
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
import re

# Simple custom validator
def validate_even_number(value):
    if value % 2 != 0:
        raise ValidationError(
            _('%(value)s is not an even number'),
            params={'value': value}
        )

# Parameterized validator
def validate_file_size(max_size_mb):
    def validator(file):
        if file.size > max_size_mb * 1024 * 1024:
            raise ValidationError(
                f'File size cannot exceed {max_size_mb}MB'
            )
    return validator

# Password strength validator
def validate_password_strength(password):
    if len(password) < 8:
        raise ValidationError('Password must be at least 8 characters long')
    
    if not re.search(r'[A-Z]', password):
        raise ValidationError('Password must contain at least one uppercase letter')
    
    if not re.search(r'[a-z]', password):
        raise ValidationError('Password must contain at least one lowercase letter')
    
    if not re.search(r'\d', password):
        raise ValidationError('Password must contain at least one digit')
    
    if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
        raise ValidationError('Password must contain at least one special character')

# Business rule validator
def validate_future_date(date):
    from django.utils import timezone
    if date <= timezone.now().date():
        raise ValidationError('Date must be in the future')

# Using custom validators in models
class Event(models.Model):
    title = models.CharField(max_length=200)
    
    attendees = models.IntegerField(
        validators=[validate_even_number]
    )
    
    event_date = models.DateField(
        validators=[validate_future_date]
    )
    
    image = models.ImageField(
        upload_to='events/',
        validators=[validate_file_size(5)]  # Max 5MB
    )

# Class-based validator
class MinimumAgeValidator:
    def __init__(self, min_age=18):
        self.min_age = min_age
    
    def __call__(self, birth_date):
        from django.utils import timezone
        from datetime import timedelta
        
        age = (timezone.now().date() - birth_date).days / 365.25
        if age < self.min_age:
            raise ValidationError(
                f'Minimum age is {self.min_age} years',
                code='minimum_age'
            )

class UserProfile(models.Model):
    birth_date = models.DateField(
        validators=[MinimumAgeValidator(18)]
    )

# Conditional validator
def validate_discount(value):
    from datetime import date
    if date.today().month == 12:  # December
        if value > 30:
            raise ValidationError('Max discount in December is 30%')
    else:
        if value > 20:
            raise ValidationError('Max discount is 20%')

Model clean Methods

Model clean methods provide model-level validation enabling cross-field validation and complex business logic beyond individual field validators. The clean method runs when full_clean() is called validating entire model instances. Understanding clean methods enables implementing sophisticated validation requiring access to multiple fields or model state.

pythonclean_methods.py
# Model clean Methods
from django.db import models
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _

class Event(models.Model):
    title = models.CharField(max_length=200)
    start_date = models.DateTimeField()
    end_date = models.DateTimeField()
    capacity = models.IntegerField()
    registered = models.IntegerField(default=0)
    
    def clean(self):
        # Cross-field validation
        if self.end_date <= self.start_date:
            raise ValidationError({
                'end_date': 'End date must be after start date'
            })
        
        # Business logic validation
        if self.registered > self.capacity:
            raise ValidationError({
                'registered': 'Registered attendees cannot exceed capacity'
            })
        
        # Complex validation
        from django.utils import timezone
        if self.start_date < timezone.now():
            if not self.pk:  # New event
                raise ValidationError('Cannot create event in the past')
    
    def save(self, *args, **kwargs):
        # Call full_clean before saving
        self.full_clean()
        super().save(*args, **kwargs)

# Multiple validation errors
class Product(models.Model):
    name = models.CharField(max_length=200)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    sale_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
    stock = models.IntegerField()
    min_stock = models.IntegerField()
    
    def clean(self):
        errors = {}
        
        # Validate sale price
        if self.sale_price:
            if self.sale_price >= self.price:
                errors['sale_price'] = 'Sale price must be less than regular price'
            
            if self.sale_price <= 0:
                errors['sale_price'] = 'Sale price must be positive'
        
        # Validate stock levels
        if self.stock < 0:
            errors['stock'] = 'Stock cannot be negative'
        
        if self.min_stock > self.stock:
            errors['min_stock'] = 'Minimum stock cannot exceed current stock'
        
        if errors:
            raise ValidationError(errors)

# Field-specific clean methods
class Post(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    status = models.CharField(max_length=20)
    published_date = models.DateTimeField(null=True, blank=True)
    
    def clean_slug(self):
        # Clean specific field
        if 'admin' in self.slug.lower():
            raise ValidationError('Slug cannot contain "admin"')
    
    def clean(self):
        # Call field-specific clean
        self.clean_slug()
        
        # Validate published posts must have date
        if self.status == 'published' and not self.published_date:
            raise ValidationError({
                'published_date': 'Published posts must have a published date'
            })

# Conditional validation based on instance state
class Order(models.Model):
    status = models.CharField(max_length=20)
    total = models.DecimalField(max_digits=10, decimal_places=2)
    paid_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    
    def clean(self):
        # Only validate for existing orders
        if self.pk:
            # Check if status change is valid
            try:
                old_instance = Order.objects.get(pk=self.pk)
                if old_instance.status == 'completed' and self.status != 'completed':
                    raise ValidationError(
                        'Cannot change status of completed order'
                    )
            except Order.DoesNotExist:
                pass
        
        # Validate payment
        if self.status == 'completed' and self.paid_amount < self.total:
            raise ValidationError('Order cannot be completed with incomplete payment')

# Using clean in views
from django.shortcuts import render, redirect

def create_event_view(request):
    if request.method == 'POST':
        event = Event(
            title=request.POST['title'],
            start_date=request.POST['start_date'],
            end_date=request.POST['end_date'],
            capacity=request.POST['capacity']
        )
        
        try:
            event.full_clean()  # Validates model
            event.save()
            return redirect('event_detail', pk=event.pk)
        except ValidationError as e:
            return render(request, 'event_form.html', {
                'errors': e.message_dict
            })

Form Validation Integration

Django forms integrate model validation automatically calling full_clean() when forms validate. ModelForms inherit field validators and clean methods from models ensuring consistent validation across application layers. Understanding form validation integration enables building complete validation workflows from user input to database storage.

pythonform_validation.py
# Form Validation Integration
from django import forms
from django.core.exceptions import ValidationError
from .models import Event, Product

# ModelForm with model validation
class EventForm(forms.ModelForm):
    class Meta:
        model = Event
        fields = ['title', 'start_date', 'end_date', 'capacity']
    
    def clean(self):
        # Form-level validation
        cleaned_data = super().clean()
        
        # Additional form validation
        title = cleaned_data.get('title')
        if title and 'test' in title.lower():
            raise ValidationError('Event title cannot contain "test"')
        
        return cleaned_data
    
    # Field-specific validation
    def clean_capacity(self):
        capacity = self.cleaned_data['capacity']
        if capacity < 10:
            raise ValidationError('Minimum capacity is 10 people')
        return capacity

# Form with custom validation
class ProductForm(forms.ModelForm):
    class Meta:
        model = Product
        fields = ['name', 'price', 'sale_price', 'stock']
    
    def clean(self):
        cleaned_data = super().clean()
        price = cleaned_data.get('price')
        sale_price = cleaned_data.get('sale_price')
        
        # Cross-field validation
        if sale_price and price:
            if sale_price >= price:
                raise ValidationError(
                    'Sale price must be less than regular price'
                )
        
        return cleaned_data

# View with form validation
def create_product_view(request):
    if request.method == 'POST':
        form = ProductForm(request.POST)
        if form.is_valid():
            # form.is_valid() calls full_clean() on model
            product = form.save()
            return redirect('product_detail', pk=product.pk)
        else:
            # Display form errors
            return render(request, 'product_form.html', {'form': form})
    else:
        form = ProductForm()
    
    return render(request, 'product_form.html', {'form': form})

# Handling validation errors in templates
# <form method="post">
#     {% csrf_token %}
#     {{ form.as_p }}
#     {% if form.non_field_errors %}
#         <ul class="errors">
#         {% for error in form.non_field_errors %}
#             <li>{{ error }}</li>
#         {% endfor %}
#         </ul>
#     {% endif %}
#     <button type="submit">Save</button>
# </form>

Validation Best Practices

Effective validation follows patterns ensuring data integrity and user experience. Use built-in validators when possible avoiding custom code. Implement field validators for single-field rules and clean methods for cross-field validation. Always call full_clean() before saving when not using forms. Provide clear, actionable error messages helping users correct issues. Validate data as early as possible at form level when available. Use custom validators for reusable validation logic. Handle ValidationError exceptions appropriately displaying errors to users. Test validation thoroughly covering edge cases and invalid inputs. Document validation rules clearly for maintainers. These practices ensure robust data validation supporting reliable applications with proper data quality and user-friendly error handling from simple forms to complex business logic validation.

$ cat /comments/ (0)

new_comment.sh

// Email hidden from public

>_

$ cat /comments/

// No comments found. Be the first!

[session] guest@{codershandbook}[timestamp] 2026

Navigation

Connect

Subscribe

// 2026 {Coders Handbook}. EOF.