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:
- Cleaning: The input value is passed through
clean(value). By default, this returns the value as-is, but subclasses likeMinLengthValidatoroverride this to return the length of the value instead. - Limit Resolution: The
limit_valueprovided during initialization is resolved. If the limit is a callable, it is executed at validation time. This allows for dynamic limits, such as aMaxValueValidatorthat always checks against the current time. - Comparison: The cleaned value and the resolved limit are passed to
compare(a, b). - Validation Failure: If
comparereturnsTrue, aValidationErroris raised using the configuredmessageandcode.
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:
MaxValueValidatorandMinValueValidatorcompare values directly. These are used extensively in model fields likeIntegerFieldto enforce range constraints. - Length Validation:
MinLengthValidatorandMaxLengthValidatoroverride thecleanmethod to validate the size of a collection or string:def clean(self, x):
return len(x) - Step Validation:
StepValueValidatorensures a value is a multiple of a specific step size. It usesmath.remainderandmath.iscloseto handle floating-point precision safely, ensuring that values like0.55are correctly validated against a step of0.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:
- Model and Form Fields: Fields maintain a
validatorslist. When a field'sclean()method is called, it iterates through this list, calling each validator with the current value. - 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.