Data Transformation Fundamentals
When building APIs with Django, the database layer returns QuerySet objects, whereas the client typically expects data in JSON format. Serialization bridges this gap by converting complex data structures into native Python types, which can then be easily rendered into JSON.
Before utilizing the built-in serializers, a manual approach might involve iterating over the QuerySet and converting values:
import
from django.http import HttpResponse
from .models import Project
class ProjectListView(APIView):
def get(self, request):
# Manual conversion
projects = list(Project.objects.all().values('id', 'name'))
return HttpResponse(.dumps(projects), content_type='application/')
Implementing Basic Serializers
DRF provides a Serializer class that handles the conversion logic. To use it, define a class inheriting from serializers.Serializer and declare fields corresponding to the model attributes.
from rest_framework import serializers
class ProjectSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
status = serializers.CharField()
In the view, instantiate the serializer by passing the QuerySet as the instance argument. If handling multiple records, set many=True.
class ProjectListView(APIView):
def get(self, request):
queryset = Project.objects.all()
serializer = ProjectSerializer(instance=queryset, many=True)
return Response(serializer.data)
Handling Complex Field Types
Simple fields map directly, but relationships and choice fields require specific configurations.
Displaying Choice Labels
For fields with defined choices, use the source parameter to access the display method (e.g., get_status_display).
class ProjectSerializer(serializers.Serializer):
status = serializers.CharField(source="get_status_display")
Handling Foreign Keys
To display a related object's attribute, use dot notation in the source argument (e.g., team.name).
class ProjectSerializer(serializers.Serializer):
team_name = serializers.CharField(source='team.name')
Handling Many-to-Many Relationships
For many-to-many fields, use SerializerMethodField and define a corresponding method prefixed with get_ to return custom data structures.
class ProjectSerializer(serializers.Serializer):
contributors = serializers.SerializerMethodField()
def get_contributors(self, obj):
return [{'id': c.id, 'name': c.name} for c in obj.contributors.all()]
Leveraging ModelSerializer
The ModelSerializer class provides a shortcut to create serializers based on model definitions. It automatically infers fields and validators.
class ProjectModelSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = "__all__"
# Or specific fields: fields = ['id', 'name', 'status']
You can mix standard fields with method fields within a ModelSerializer.
Automatic Nested Serialization (Depth)
To handle nested relationships automatically without explicitly defining nested serializers, the depth option can be used in the Meta class. This creates a nested representation of related primary keys up to the specified depth.
class ProjectModelSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = ['id', 'name', 'team', 'contributors']
depth = 1
Generating Hyperlinks
RESTful APIs often require linking related resources. The HyperlinkedIdentityField generates a URL instead of a primary key.
class ProjectSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='project-detail', lookup_field='pk')
team_url = serializers.HyperlinkedIdentityField(view_name='team-detail', lookup_field='team_id')
class Meta:
model = Project
fields = ['url', 'name', 'team_url']
Input Validation
Serializers are responsible for validating incoming data. The is_valid() method triggers validation checks.
Field-Level Validation
Define a method named validate_<fieldname> to validate specific fields.
def validate_name(self, value):
if 'deprecated' in value.lower():
raise serializers.ValidationError("Project name cannot contain 'deprecated'.")
return value
Object-Level Validation
To validate multiple fields together, override the validate method.
def validate(self, data):
if data['start_date'] > data['end_date']:
raise serializers.ValidationError("End date must occur after start date.")
return data
Custom Validators
You can create reusable validator classes and pass them to the field via the validators argument.
class UniquePrefixValidator:
def __init__(self, prefix):
self.prefix = prefix
def __call__(self, value):
if not value.startswith(self.prefix):
raise serializers.ValidationError(f"Title must start with {self.prefix}")
class ProjectSerializer(serializers.Serializer):
title = serializers.CharField(validators=[UniquePrefixValidator('REQ-')])
Controlling Read and Write Access
The write_only and read_only arguments control field behavior during serialization (reading) and deserialization (writing).
class ProjectSerializer(serializers.ModelSerializer):
# Used for creation/updating but not included in response
secret_key = serializers.CharField(write_only=True)
# Calculated field only included in response
full_name = serializers.CharField(read_only=True)
class Meta:
model = Project
fields = '__all__'
Overriding Create and Update Methods
To customize how instances are saved, override create and update methods in the serializer.
class ProjectSerializer(serializers.ModelSerializer):
def create(self, validated_data):
# Extract Many-to-Many data if necessary
contributors_data = validated_data.pop('contributors')
project = Project.objects.create(**validated_data)
project.contributors.set(contributors_data)
return project
def update(self, instance, validated_data):
instance.name = validated_data.get('name', instance.name)
instance.status = validated_data.get('status', instance.status)
instance.save()
return instance
Filtering Data
For list endpoints, filtering allows clients to narrow down results. The django-filter library integrates seamlessly with DRF.
# settings.py
INSTALLED_APPS = ['django_filters']
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend']
}
from django_filters.rest_framework import FilterSet
class ProjectFilter(FilterSet):
class Meta:
model = Project
fields = {'status': ['exact'], 'name': ['icontains']}
class ProjectListView(ListAPIView):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
filterset_class = ProjectFilter