Skip to main content

Overview of the Validation API

The validation API in this codebase provides a robust framework for ensuring data integrity across models and forms. At its core, a validator is any callable that accepts a value and raises a django.core.exceptions.ValidationError if that value does not meet specific criteria.

The foundation of this framework is the BaseValidator class, located in django/core/validators.py. It establishes a consistent pattern for validators that compare a given value against a defined limit.

The BaseValidator Lifecycle

The BaseValidator implements a structured lifecycle that subclasses can hook into to define specific validation logic. When a validator is called, it follows these steps:

  1. Cleaning: The input value is passed through clean(value). By default, this returns the value as-is, but subclasses like MinLengthValidator override this to return the length of the value instead.
  2. Limit Resolution: The limit_value provided during initialization is resolved. If the limit is a callable, it is executed at validation time. This allows for dynamic limits, such as a MaxValueValidator that always checks against the current time.
  3. Comparison: The cleaned value and the resolved limit are passed to compare(a, b).
  4. Validation Failure: If compare returns True, a ValidationError is raised using the configured message and code.

The Comparison Logic

A critical detail in the BaseValidator implementation is the return value of the compare method:

# django/core/validators.py

def compare(self, a, b):
return a is not b

In this framework, compare returns True if the validation should fail. For example, in MaxValueValidator, the comparison is defined as:

class MaxValueValidator(BaseValidator):
def compare(self, a, b):
return a > b # Fails if the value is greater than the limit

Built-in Validator Implementations

The codebase leverages BaseValidator to implement several standard integrity checks:

  • Numeric/Ordered Validation: MaxValueValidator and MinValueValidator compare values directly. These are used extensively in model fields like IntegerField to enforce range constraints.
  • Length Validation: MinLengthValidator and MaxLengthValidator override the clean method to validate the size of a collection or string:
    def clean(self, x):
    return len(x)
  • Step Validation: StepValueValidator ensures a value is a multiple of a specific step size. It uses math.remainder and math.isclose to handle floating-point precision safely, ensuring that values like 0.55 are correctly validated against a step of 0.01.

Dynamic Limits and Callables

One of the most powerful features of the API is the ability to use callables for the limit_value. This is handled in the __call__ method:

# django/core/validators.py

def __call__(self, value):
# ...
limit_value = (
self.limit_value() if callable(self.limit_value) else self.limit_value
)
# ...

This allows developers to pass functions like timezone.now to a MaxValueValidator, ensuring the limit is always calculated relative to the moment validation occurs rather than when the validator was instantiated.

Extending the API

The API is designed for extension. A common pattern in this codebase is to subclass an existing validator to handle specialized data types. For instance, django.contrib.postgres.validators.RangeMaxValueValidator extends MaxValueValidator to handle PostgreSQL range types:

class RangeMaxValueValidator(MaxValueValidator):
def compare(self, a, b):
return a.upper is None or a.upper > b

message = _(
"Ensure that the upper bound of the range is not greater than %(limit_value)s."
)

Integration and Serialization

Validators are integrated into the system in two primary ways:

  1. Model and Form Fields: Fields maintain a validators list. When a field's clean() method is called, it iterates through this list, calling each validator with the current value.
  2. Migrations: To ensure validators can be serialized into migration files, they are decorated with @deconstructible. This allows the Django migration framework to recreate the validator instance with its original arguments.
@deconstructible
class BaseValidator:
# ...

When creating custom validators, applying the @deconstructible decorator is essential if the validator is intended to be used directly on a Model field.