Django REST Framework gives you several layers to validate serializer input, and they run in a fixed order: each field's own type coercion and validators=[...] list, then your per-field validate_<fieldname> methods, then serializer-level validators (such as UniqueTogetherValidator), and finally the object-level validate(self, data) method. This guide is a deep, practical tour of every mechanism: field-level checks, cross-field rules, reusable validator functions and classes (including request-aware validators), the built-in validators that ship with DRF, how to raise serializers.ValidationError with field-keyed messages and error codes, and how required, read_only, and default change what actually gets validated. There is a decision table at the end so you can pick the right tool for each rule.
This is the dedicated validation companion to our broader walkthrough on customizing DRF serializers — start there for nested serializers, to_representation(), and computed fields, and stay here for everything validation.
The order DRF runs validation
Validation is triggered when you call serializer.is_valid(). Internally DRF runs a single pipeline (run_validation) that proceeds in this exact order:
- Per field (inside
to_internal_value): the field coerces the raw value to a Python type (to_internal_value), runs the field's ownvalidators=[...]list, and then yourvalidate_<fieldname>method on the serializer (if one exists). - Serializer-level validators: any validators declared on
Meta.validatorsor returned by the serializer, e.g.UniqueTogetherValidator. - Object-level: your
validate(self, data)method, which receives the dict of already-validated fields.
Two consequences fall out of this order. First, validate_<fieldname> only ever sees a value that has already passed type coercion, so you never have to defensively parse strings into dates or ints. Second, validate(self, data) only runs if every field validated successfully, so cross-field rules can assume the individual fields are sound.
Field-level validation: per-field validate_ methods
For a rule that belongs to a single field, add a validate_<fieldname> method. It receives the already-coerced value, must return it on success, and must raise serializers.ValidationError on failure. The error is automatically keyed to that field.
from datetime import date
from rest_framework import serializers
class JobApplicationSerializer(serializers.Serializer):
email = serializers.EmailField()
name = serializers.CharField(max_length=200)
date_of_birth = serializers.DateField()
def validate_date_of_birth(self, value):
today = date.today()
age = today.year - value.year - ((today.month, today.day) < (value.month, value.day))
if not (20 <= age < 30):
raise serializers.ValidationError("Applicants must be between 20 and 29 years old.")
return value# Invalid input -> the error is keyed to the field automatically
serializer = JobApplicationSerializer(data={
"email": "dev@example.com",
"name": "Sam",
"date_of_birth": "1980-04-08",
})
serializer.is_valid() # False
serializer.errors # {'date_of_birth': ['Applicants must be between 20 and 29 years old.']}Object-level validation: validate(self, data)
When a rule spans two or more fields, you cannot express it at the field level — by the time a field method runs, the other fields are not yet available. Override validate(self, data) instead. It receives a dict of the validated fields and must return it (returning the data is mandatory — forget it and validated_data comes back empty).
class DateRangeSerializer(serializers.Serializer):
start_date = serializers.DateField()
end_date = serializers.DateField()
def validate(self, data):
if data["end_date"] < data["start_date"]:
# A dict raises a *field* error; a string/list raises a non_field_errors error.
raise serializers.ValidationError({"end_date": "End date must be after the start date."})
return dataRaising a plain string or list from validate() produces a non_field_errors entry. Raising a dict lets you attach the message to a specific field key, which is almost always the better UX because the frontend can show the error next to the right input.
Reusable validator functions: the validators=[...] option
If the same rule appears on many fields or many serializers, extract it into a callable and pass it through the field's validators list (or Meta.validators for object-level reuse). A validator function takes the value, raises on failure, and returns nothing meaningful (its return value is ignored — unlike validate_<fieldname>, which must return the value).
from rest_framework import serializers
def even_number(value):
if value % 2 != 0:
raise serializers.ValidationError("This value must be an even number.", code="not_even")
class TeamSerializer(serializers.Serializer):
name = serializers.CharField(max_length=120)
head_count = serializers.IntegerField(validators=[even_number])Order matters here too: a field's validators list runs before the serializer's validate_<fieldname> method for that field. Note also that a field's validators list is skipped when the value is supplied by a default (see the required/default section below).
Validator classes and request-aware validation
A plain function can't be configured. When a rule needs parameters or access to the request, write a validator class with a __call__ method. To reach the serializer context (and therefore the request), set the class attribute requires_context = True; DRF then calls __call__(self, value, serializer_field), passing the bound field whose .context exposes the request.
from rest_framework import serializers
class MultipleOf:
"""Configurable: reusable across fields with different bases."""
def __init__(self, base):
self.base = base
def __call__(self, value):
if value % self.base != 0:
raise serializers.ValidationError(f"Must be a multiple of {self.base}.", code="not_multiple")
class OwnedByCurrentUser:
"""Request-aware: only the owner may reference this object."""
requires_context = True
def __call__(self, value, serializer_field):
request = serializer_field.context.get("request")
if request and value.owner_id != request.user.id:
raise serializers.ValidationError("You do not own this resource.", code="not_owner")
class OrderLineSerializer(serializers.Serializer):
quantity = serializers.IntegerField(validators=[MultipleOf(6)])
warehouse = serializers.PrimaryKeyRelatedField(
queryset=Warehouse.objects.all(),
validators=[OwnedByCurrentUser()],
)You normally pass the request into the serializer from your view via get_serializer_context() (DRF's generic views do this for you). For passing additional context beyond the request, see our dedicated post on passing extra context to DRF serializers.
Built-in validators that ship with DRF
DRF (and Django core) provide ready-made validators so you rarely write uniqueness or range checks by hand.
UniqueValidator— enforces a single field is unique against a queryset.UniqueTogetherValidator— enforces a combination of fields is unique; declare it onMeta.validators.MaxValueValidator/MinValueValidator,MaxLengthValidator,RegexValidator— Django's core validators work directly on DRF fields.
For ModelSerializer, DRF generates UniqueValidator and UniqueTogetherValidator automatically from your model's unique=True and unique_together / UniqueConstraint definitions — so you often get them for free.
from django.core.validators import MaxValueValidator, MinValueValidator
from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator, UniqueValidator
class BookingSerializer(serializers.ModelSerializer):
guest_email = serializers.EmailField(
validators=[UniqueValidator(
queryset=Booking.objects.all(),
message="This guest already has a booking.",
)]
)
nights = serializers.IntegerField(
validators=[MinValueValidator(1), MaxValueValidator(30)]
)
class Meta:
model = Booking
fields = ["id", "room", "date", "guest_email", "nights"]
validators = [
UniqueTogetherValidator(
queryset=Booking.objects.all(),
fields=["room", "date"],
message="That room is already booked for the selected date.",
)
]Raising ValidationError: messages, dicts, and codes
serializers.ValidationError accepts a string, a list of strings, or a dict mapping field names to messages. Always pass a machine-readable code so API clients (and your tests) can branch on the failure type rather than parse English.
# Single message with a code
raise serializers.ValidationError("Required for paid plans.", code="required_for_paid")
# Multiple field errors at once, from an object-level validate()
raise serializers.ValidationError({
"password": "Too short.",
"password_confirm": "Passwords do not match.",
})
# The code surfaces in the structured error detail:
# {'plan': [ErrorDetail(string='Required for paid plans.', code='required_for_paid')]}How required, read_only, and default change validation
These three field options decide whether a value even reaches your validators:
read_only=True— the field is excluded fromvalidated_dataentirely, so no field validators and novalidate_<fieldname>run for it. Never put input validation on a read-only field.required=True(the default for most fields) — a missing value fails with arequirederror before any custom validation runs.required=Falsewith no default — a missing value is dropped (the field is skipped), so itsvalidate_<fieldname>is not called and the key is absent invalidate(self, data). Usedata.get("field"), notdata["field"].default=...— when the value is omitted, the default is injected. The field's ownvalidatorslist is skipped for a defaulted value, but the serializer'svalidate_<fieldname>still runs on it.
The same skipping behaviour applies to partial updates (partial=True, i.e. PATCH): omitted fields are skipped, so guard every key access inside validate().
Which validation tool should you use?
| Mechanism | Receives | Reusable? | Best for |
|---|---|---|---|
Field method (validate_<field>) |
the single coerced field value | No (tied to one serializer) | A one-off rule for one specific field |
Object method (validate(self, data)) |
dict of all validated fields | No (tied to one serializer) | Cross-field / multi-field rules |
Validator function (in validators=[...]) |
a single value | Yes | A simple rule reused across fields/serializers |
Validator class (__call__) |
value (+ field, if requires_context) |
Yes | Configurable, stateful, or request-aware rules |
Built-in (UniqueValidator, etc.) |
depends on validator | Yes | Uniqueness, ranges, regex, length |
Rule of thumb: reach for a validate_<fieldname> method first; promote it to a reusable function or class the moment a second field or serializer needs the same rule; and use validate(self, data) whenever the rule depends on more than one field.
Real-world example: validating an order
Here is an order serializer that combines almost every technique — a nested serializer with field-level validation, a cross-field object-level rule, and request-aware logic via context.
from rest_framework import serializers
class OrderItemSerializer(serializers.Serializer):
sku = serializers.CharField()
quantity = serializers.IntegerField(min_value=1)
class OrderSerializer(serializers.Serializer):
items = OrderItemSerializer(many=True)
coupon = serializers.CharField(required=False, allow_blank=True)
use_loyalty_points = serializers.BooleanField(default=False)
def validate_items(self, items):
if not items:
raise serializers.ValidationError("An order needs at least one item.")
skus = [item["sku"] for item in items]
if len(skus) != len(set(skus)):
raise serializers.ValidationError("Duplicate SKUs aren't allowed; merge the quantities instead.")
return items
def validate(self, data):
request = self.context.get("request")
if data.get("use_loyalty_points") and not (request and request.user.is_authenticated):
raise serializers.ValidationError({"use_loyalty_points": "Sign in to redeem loyalty points."})
if data.get("coupon") and data["use_loyalty_points"]:
raise serializers.ValidationError("A coupon and loyalty points can't be combined on one order.")
return dataCommon pitfalls
- Returning instead of raising. Validator functions and
validate_*methods signal failure by raisingValidationError. ReturningFalseorNonedoes nothing — the value is accepted. - Forgetting to return in
validate().validate(self, data)andvalidate_<fieldname>must return the (possibly cleaned) value. Omit the return and the data silently disappears fromvalidated_data. - Indexing missing keys. On
required=Falsefields andPATCHrequests, keys can be absent invalidate(). Usedata.get(...). UniqueTogetherValidatorquietly skips. If any of its fields is missing or not required (common on partial updates), it skips the check entirely, so duplicates can slip through. Validate uniqueness explicitly forPATCHif it matters.- Putting rules on read-only fields. They never reach validation; the rule will never fire.
- Mixing up Django and DRF errors. Inside custom serializer methods, raise
rest_framework.serializers.ValidationError, notdjango.core.exceptions.ValidationError.
Frequently Asked Questions
What is the difference between field-level and object-level validation in DRF?
Field-level validation (validate_<fieldname>) checks one field in isolation and receives only that field's coerced value. Object-level validation (validate(self, data)) runs after all fields pass and receives a dict of every validated field, so it is the only place to enforce rules that depend on more than one field — for example, that an end date is after a start date, or that two passwords match.
In what order does DRF run serializer validation?
When you call is_valid(), DRF runs, per field, the field's type coercion and its validators=[...] list, then your validate_<fieldname> method. After all fields pass, it runs serializer-level validators (such as UniqueTogetherValidator on Meta.validators), and finally validate(self, data). Object-level validation only runs if every field validated successfully.
When should I use a validator function instead of a validate_ method?
Use a validate_<fieldname> method for a rule that lives on one field of one serializer. Extract the rule into a reusable validator function (and pass it via validators=[...]) the moment a second field or serializer needs the same check. Upgrade the function to a validator class when the rule needs configuration or access to the request.
How do I access the request inside a serializer validator?
Inside validate() or a validate_<fieldname> method, use self.context.get("request"). Inside a validator class, set requires_context = True and DRF will call __call__(self, value, serializer_field), where serializer_field.context exposes the request. The request reaches the serializer through the view's serializer context, which DRF's generic views populate automatically.
Why is my object-level validate method not catching a missing field?
If a field is required=False with no default, or the request is a partial PATCH, an omitted field is skipped and its key never appears in the data dict passed to validate(). Accessing it with data["field"] raises KeyError; use data.get("field") and handle the None case explicitly.
How do I return a validation error tied to a specific field from validate()?
Raise serializers.ValidationError with a dict, e.g. raise serializers.ValidationError({"end_date": "Must be after start date."}). A dict keys the error to that field; a plain string or list instead produces a generic non_field_errors entry that the frontend cannot attach to a particular input.
Building DRF APIs with MicroPyramid
Validation is where most API bugs and security gaps hide, so getting these layers right pays off across every endpoint. For 12+ years and across 50+ delivered projects, MicroPyramid has built and maintained Django and Django REST Framework APIs with rigorous, well-tested validation. If you want a hand designing serializers, validation rules, or a full API, explore our Django development, Python development, and custom software development services.