Skip to content

Serializers

Serializers are the core component of DRF, handling data conversion in two directions: converting model objects to JSON (serialization), and validating and converting JSON submitted by clients back to model objects (deserialization). DRF provides two types of serializers: Serializer, where fields are declared manually, and ModelSerializer, which maps automatically to a model.

Serializer Basics

Defining a Serializer

All serializers inherit from rest_framework.serializers.Serializer. Field declarations are similar to Django Forms — each field corresponds to a data type:

from rest_framework import serializers

class StudentSerializer(serializers.Serializer):
    id          = serializers.IntegerField(read_only=True)
    name        = serializers.CharField(max_length=100)
    age         = serializers.IntegerField(min_value=0, max_value=150)
    sex         = serializers.BooleanField(default=True)
    description = serializers.CharField(required=False, allow_blank=True)
Serializers are decoupled from database models. You can define a serializer for any Python object, including non-ORM data.

Common Field Types

Field TypeDescription
CharFieldString; supports max_length, min_length
IntegerFieldInteger; supports max_value, min_value
FloatFieldFloat
DecimalFieldFixed-point number; requires max_digits, decimal_places
BooleanFieldBoolean
DateFieldDate (YYYY-MM-DD)
DateTimeFieldDate and time
EmailFieldEmail; automatic format validation
URLFieldURL; automatic format validation
UUIDFieldUUID
ChoiceFieldEnum; requires choices
ListFieldList; requires child field child
DictFieldDictionary; requires child field child
SerializerMethodFieldRead-only computed field; value returned by a get_<field_name> method

Common Field Parameters

ParameterDefaultDescription
read_onlyFalseUsed for serialization output only; ignored during deserialization
write_onlyFalseUsed for deserialization input only; hidden in serialization output
requiredTrueWhether the field must be provided during deserialization
defaultDefault value when not provided
allow_nullFalseWhether null/None is allowed
allow_blankFalseWhether an empty string is allowed (CharField only)
validators[]Additional validator functions
error_messagesCustom error message dictionary
labelField name displayed in the visual interface
help_textHelp text displayed in the visual interface

Serialization (Model → JSON)

Serializing a Single Object

from students.models import Student
from .serializers import StudentSerializer

student = Student.objects.get(pk=1)
serializer = StudentSerializer(instance=student)
print(serializer.data)
# {'id': 1, 'name': 'Zhang San', 'age': 20, 'sex': True, 'description': '...'}

Returning from a view:

from django.views import View
from django.http import JsonResponse
from students.models import Student
from .serializers import StudentSerializer

class StudentView(View):
    def get(self, request, pk):
        student = Student.objects.get(pk=pk)
        serializer = StudentSerializer(instance=student)
        return JsonResponse(serializer.data)

Serializing Multiple Objects

When the data source is a QuerySet, pass many=True:

students = Student.objects.all()
serializer = StudentSerializer(instance=students, many=True)
# serializer.data is a list
return JsonResponse(serializer.data, safe=False)

SerializerMethodField Example

Used to add computed fields to the output:

class StudentSerializer(serializers.Serializer):
    id   = serializers.IntegerField(read_only=True)
    name = serializers.CharField()
    age  = serializers.IntegerField()
    # Computed field: whether the student is an adult based on age
    is_adult = serializers.SerializerMethodField()

    def get_is_adult(self, obj):
        return obj.age >= 18

Deserialization (JSON → Model)

Data Validation

Deserialization flow: receive client data → instantiate the serializer (pass data) → call is_valid() → access validated_data.

data = {
    "name": "Li Si",
    "age": 22,
    "sex": True,
    "description": "Test user",
}
serializer = StudentSerializer(data=data)

if serializer.is_valid():
    print(serializer.validated_data)  # OrderedDict containing validated data
else:
    print(serializer.errors)          # Dictionary containing field errors

To raise an HTTP 400 directly on validation failure:

serializer.is_valid(raise_exception=True)

Custom Validation (Hooks)

DRF provides three ways to extend validation, executed in priority order from highest to lowest:

1. Field-level hook validate_<field_name>

class StudentSerializer(serializers.Serializer):
    name = serializers.CharField()

    def validate_name(self, value):
        if value == "admin":
            raise serializers.ValidationError("Username cannot be 'admin'")
        return value  # Must return the validated value

2. Object-level hook validate

Used for cross-field combined validation:

def validate(self, data):
    if data.get("age", 0) < 18 and data.get("sex") is False:
        raise serializers.ValidationError("Female minors are not allowed to register")
    return data  # Must return data

3. validators functions

Attach independent validator functions to fields for easy reuse:

def check_age(value):
    if value == 0:
        raise serializers.ValidationError("Age cannot be 0")
    return value

class StudentSerializer(serializers.Serializer):
    age = serializers.IntegerField(validators=[check_age])

Saving Data (create / update)

When serializer.save() is called, DRF dispatches automatically to create() or update() depending on whether instance was passed. These two methods must be implemented in the serializer:

class StudentSerializer(serializers.Serializer):
    id          = serializers.IntegerField(read_only=True)
    name        = serializers.CharField(required=True, max_length=100)
    age         = serializers.IntegerField(min_value=0, max_value=150)
    sex         = serializers.BooleanField(default=True)
    description = serializers.CharField(required=False, allow_blank=True)

    def create(self, validated_data):
        return Student.objects.create(**validated_data)

    def update(self, instance, validated_data):
        instance.name        = validated_data.get("name", instance.name)
        instance.age         = validated_data.get("age", instance.age)
        instance.sex         = validated_data.get("sex", instance.sex)
        instance.description = validated_data.get("description", instance.description)
        instance.save()
        return instance

Using it in a view:

# Create (no instance passed)
serializer = StudentSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
student = serializer.save()   # Calls create()

# Update (instance passed)
student = Student.objects.get(pk=pk)
serializer = StudentSerializer(instance=student, data=request.data)
serializer.is_valid(raise_exception=True)
student = serializer.save()   # Calls update()

# Partial update (PATCH)
serializer = StudentSerializer(instance=student, data=request.data, partial=True)

save() also accepts extra keyword arguments that are merged into validated_data:

serializer.save(created_by=request.user)

ModelSerializer

ModelSerializer inherits from Serializer and automatically generates fields from the model class. It also includes built-in create() and update() implementations, greatly reducing boilerplate code.

Basic Definition

from rest_framework import serializers
from .models import Student

class StudentModelSerializer(serializers.ModelSerializer):
    class Meta:
        model  = Student
        fields = "__all__"   # Include all model fields

Meta Configuration Options

class StudentModelSerializer(serializers.ModelSerializer):
    class Meta:
        model  = Student

        # Option 1: list the required fields
        fields = ["id", "name", "age", "sex", "description"]

        # Option 2: exclude specific fields (mutually exclusive with fields; cannot use both)
        # exclude = ["description"]

        # Read-only field list (equivalent to setting read_only=True on each field)
        read_only_fields = ("id",)

        # Add or override field options
        extra_kwargs = {
            "sex":         {"write_only": True},
            "description": {"required": False, "allow_blank": True},
        }

Adding Extra Fields

ModelSerializer also supports declaring additional fields (which must be listed in fields):

class StudentModelSerializer(serializers.ModelSerializer):
    full_label = serializers.SerializerMethodField()

    class Meta:
        model  = Student
        fields = ["id", "name", "age", "sex", "full_label"]

    def get_full_label(self, obj):
        return f"{obj.name} (Class {obj.class_null})"

Passing Extra Context

Sometimes you need to access request, the current user, or other information inside a serializer. Pass it from the view via context and access it inside the serializer using self.context:

# View layer
serializer = StudentModelSerializer(
    instance=student,
    context={"request": request, "project_id": 42}
)

# Inside the serializer
class StudentModelSerializer(serializers.ModelSerializer):
    class Meta:
        model  = Student
        fields = "__all__"

    def validate(self, data):
        request = self.context.get("request")
        if not request.user.is_staff:
            raise serializers.ValidationError("You do not have permission to perform this action")
        return data
When using GenericAPIView and its subclasses, the get_serializer() method automatically injects request, view, and format into context, so you do not need to pass them manually.

Serializer vs ModelSerializer

ComparisonSerializerModelSerializer
Field declarationDeclared manually, one by oneAuto-generated from model
create/updateMust be implemented manuallyBuilt-in default implementation
Use casesNon-model data, highly customized APIsStandard CRUD, closely aligned with model fields
Code volumeMoreLess; rapid development

For most standard CRUD interfaces, choose ModelSerializer. When serializing data from multiple models or needing complex field calculations, choose Serializer.

Last updated on