~/TechPurAI
~/tutorials/django-rest-framework/serializer-validation
intermediate·part 11 of 22·2 min read

Serializer validation: field-level and object-level

Updated Aug 16, 2026Python · Django

ModelSerializer validates field types and model-level constraints (max_length, required) automatically — nothing so far checks anything more specific, like "a due date can't be in the past." That's what custom validation methods are for.

Field-level validation

python
# tasks/serializers.py
from datetime import date
from rest_framework import serializers
from .models import Task

class TaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = Task
        fields = ["id", "title", "description", "completed", "due_date", "owner", "created_at"]
        read_only_fields = ["owner", "created_at"]

    def validate_due_date(self, value):
        if value and value < date.today():
            raise serializers.ValidationError("Due date can't be in the past.")
        return value

validate_<field_name> — the method name has to match the field exactly — runs after that field's own type validation passes, and only for that one field. It receives the already-type-validated value and must return it (or a transformed version of it); raising serializers.ValidationError inside it attaches the error specifically to due_date in the response, not to the request as a whole.

Object-level validation

python
    def validate(self, data):
        if data.get("completed") and not data.get("title", "").strip():
            raise serializers.ValidationError("A completed task needs a title.")
        return data

A plain validate() method (no field name) runs after every individual field has already passed its own validation, and receives the full set of validated data at once — the right place for a rule that depends on more than one field together, which no single validate_<field> could express on its own.

What the client sees

json
{"due_date": ["Due date can't be in the past."]}
json
{"non_field_errors": ["A completed task needs a title."]}

A validate_<field> error keys itself to that field automatically — exactly the shape a frontend needs to show an error next to the right input. A validate() error, with no single field to attach to, lands under non_field_errors by default — worth knowing the name ahead of time, since it's easy to assume a cross-field error would show up somewhere else the first time you see it.

Common mistake

Putting multi-field logic inside a validate_<field> method by reaching into self.initial_data for other fields. It technically works, but initial_data is the raw, unvalidated input — reading another field from it bypasses whatever validation that field's own validate_<field> method would have applied. Cross-field logic belongs in validate(), which only ever runs after every individual field already passed.

Next: relationships — Task has an owner, but so far it only ever shows as a bare ID. Nested serializers show related data properly.

VK

Vijay Kumar

Founder of TechPurAI — writing hands-on tutorials and honest tool breakdowns.

LinkedIn ↗
← previous10. Routers: automatic URL generation for ViewSetsnext →12. Nested serializers and relationships