Skip to main content

Implementing Custom Validators

To implement custom validation logic in this project, you can use simple functions for one-off checks or subclass Django's validator classes to create reusable, configurable validation components.

Creating a Function-Based Validator

For simple logic that doesn't require configuration, define a function that takes a value and raises a ValidationError if the value is invalid.

from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _

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

# Usage in a Model or Form field
# even_field = models.IntegerField(validators=[validate_even])

Subclassing RegexValidator for Pattern Matching

When you need to validate strings against a specific pattern, subclass RegexValidator. This is the approach used for system-level validators like ASCIIUsernameValidator in django/contrib/auth/validators.py.

import re
from django.core import validators
from django.utils.deconstruct import deconstructible
from django.utils.translation import gettext_lazy as _

@deconstructible
class ASCIIUsernameValidator(validators.RegexValidator):
regex = r"^[\w.@+-]+\Z"
message = _(
"Enter a valid username. This value may contain only unaccented lowercase a-z "
"and uppercase A-Z letters, numbers, and @/./+/-/_ characters."
)
flags = re.ASCII

Key Components:

  • regex: The regular expression pattern to match.
  • message: The error message shown when validation fails.
  • flags: Standard re module flags (e.g., re.IGNORECASE). Note that if flags are used, regex must be a string.
  • @deconstructible: This decorator is required for class-based validators used in Model fields to ensure they can be serialized for migrations.

Subclassing BaseValidator for Comparisons

For validators that compare a value against a limit (like MaxValueValidator or MinLengthValidator), subclass BaseValidator. You must implement the compare method and optionally the clean method.

from django.core.validators import BaseValidator
from django.utils.translation import gettext_lazy as _

class MaxValueValidator(BaseValidator):
message = _("Ensure this value is less than or equal to %(limit_value)s.")
code = "max_value"

def compare(self, a, b):
# Return True if the validation SHOULD FAIL
return a > b

class MaxLengthValidator(BaseValidator):
# ... message and code definitions ...

def compare(self, a, b):
return a > b

def clean(self, x):
# Pre-process the value before comparison (e.g., get length)
return len(x)

How BaseValidator Works:

  1. clean(value): Transforms the input (e.g., MaxLengthValidator converts a string to its length).
  2. limit_value: The threshold passed during initialization. If limit_value is a callable, it is executed at validation time.
  3. compare(cleaned_value, limit_value): If this returns True, a ValidationError is raised using the provided message and code.

Using Specialized Built-in Validators

The codebase provides several specialized validators that can be used directly or customized via arguments:

File Extension Validation

Use FileExtensionValidator to restrict uploaded files to specific types.

from django.core.validators import FileExtensionValidator

# Usage in a model field
# document = models.FileField(validators=[FileExtensionValidator(allowed_extensions=['pdf', 'docx'])])

Step Value Validation

Use StepValueValidator to ensure numeric values follow a specific increment, optionally starting from an offset.

from django.core.validators import StepValueValidator

# Ensures value is a multiple of 5 (5, 10, 15...)
validator = StepValueValidator(limit_value=5)

# Ensures value is a multiple of 5 starting from 2 (2, 7, 12...)
validator_with_offset = StepValueValidator(limit_value=5, offset=2)

Decimal Validation

DecimalValidator allows fine-grained control over decimal precision.

from django.core.validators import DecimalValidator

# Validates up to 5 digits total, with 2 decimal places
validator = DecimalValidator(max_digits=5, decimal_places=2)

Troubleshooting and Best Practices

Migration Errors

If you use a custom class-based validator in a models.py file, you must decorate the class with @deconstructible. Without this, Django's migration framework will not be able to serialize the validator into a migration file, leading to ValueError: Cannot serialize errors.

Regex Performance

When using RegexValidator, the regex is compiled using _lazy_re_compile. If you provide a pre-compiled regex object to the constructor, ensure you do not also pass flags, as this will raise a TypeError.

Floating Point Precision

When implementing comparison logic for numbers, be aware that StepValueValidator uses math.isclose with a tolerance of 1e-9 to handle floating point inaccuracies. Follow this pattern for custom numeric validators to avoid unexpected failures with floats.

import math
# Example comparison from StepValueValidator
return not math.isclose(math.remainder(a - offset, b), 0, abs_tol=1e-9)