Skip to content

DRF Serializers

Serializers in Django REST Framework (DRF) are powerful tools that let you turn complex data, like Django model instances, into simple Python data that can then easily be turned into JSON, XML, or other formats. They also work the other way around (this is called deserialization), turning incoming data back into proper Python objects after checking that the data is valid.

To create a serializer, you usually define a class that inherits from serializers.Serializer, and list the fields you want to include from the model you are working with. Here is an example based on the Product model:

# reference models.py
from django.db import models
from django.core.validators import MinValueValidator
class Collection(models.Model):
title = models.CharField(max_length=255)
def __str__(self) -> str:
return self.title
class Product(models.Model):
title = models.CharField(max_length=255)
description = models.TextField(null=True, blank=True)
unit_price = models.DecimalField(
max_digits=6,
decimal_places=2,
validators=[MinValueValidator(1)])
collection = models.ForeignKey(
Collection,
on_delete=models.CASCADE,
related_name='products'
)
def __str__(self) -> str:
return self.title

This model represents a product with a title, description, and unit price. To create a serializer for this model, you can do the following:

from rest_framework import serializers
class ProductSerializer(serializers.Serializer):
id = serializers.IntegerField()
title = serializers.CharField(max_length=255)
unit_price = serializers.DecimalField(max_digits=6, decimal_places=2)

Now let’s see how to use this serializer inside a view:

from rest_framework.views import APIView
from rest_framework.response import Response
from .models import Product
@api_view(['GET'])
def product_list(request):
products = Product.objects.all()
serializer = ProductSerializer(products, many=True)
return Response(serializer.data)

SerializerMethodField is a read-only field that gets its value by calling a method you write inside the serializer class. This is handy when you want to add extra data to the serialized output that is not a direct model field. For example, the price including tax can be calculated from the unit_price field:

from rest_framework import serializers
class ProductSerializer(serializers.Serializer):
price_with_tax = serializers.SerializerMethodField(method_name='calculate_tax')
def calculate_tax(self, obj):
# Assuming a tax rate of 10%
return obj.unit_price * 1.1

The source argument in a serializer field lets you point to a different attribute or method on the model to fill in that field’s value. This is useful when you want to show data that is not a direct model field, such as a calculated value or a value from a related object. Example: showing unit_price as price:

from rest_framework import serializers
class ProductSerializer(serializers.Serializer):
# other fields...
price = serializers.DecimalField(
max_digits=6,
decimal_places=2,
source='unit_price' # this is the source
)

By default, every field in a serializer works both ways, meaning it can be used for both serialization and deserialization. But you can make a field read-only by setting read_only=True. This means the field will show up in the serialized output, but it will not be used when creating or updating an instance. For example:

from rest_framework import serializers
class ProductSerializer(serializers.Serializer):
# other fields...
price_with_tax = serializers.SerializerMethodField(read_only=True)

In the same way, you can make a field write-only by setting write_only=True. This means the field will be used when creating or updating an instance, but it will not show up in the serialized output. For example:

from rest_framework import serializers
class ProductSerializer(serializers.Serializer):
# other fields...
secret_code = serializers.CharField(max_length=100, write_only=True)

When your models have relationships between them (like foreign keys), DRF gives you several ways to show these relationships inside your serializers. The most common ones are PrimaryKeyRelatedField, StringRelatedField, HyperlinkedRelatedField, and nested serializers.

When you have a foreign key relationship in your model, you can use PrimaryKeyRelatedField to show the related object using just its primary key (its ID). For example, if you want to include the collection field inside ProductSerializer, you can do it like this:

from rest_framework import serializers
class ProductSerializer(serializers.Serializer):
# other fields...
collection = serializers.PrimaryKeyRelatedField(queryset=Collection.objects.all())

If you want to show the related object using its string form (the one defined by the __str__ method on the model), you can use StringRelatedField. For example:

from rest_framework import serializers
class ProductSerializer(serializers.Serializer):
# other fields...
collection = serializers.StringRelatedField()

If you want to show the related object as a clickable link to its detail page, you can use HyperlinkedRelatedField. This needs a URL pattern already set up for the related model. For example:

from rest_framework import serializers
class ProductSerializer(serializers.Serializer):
# other fields...
collection = serializers.HyperlinkedRelatedField(
queryset=Collection.objects.all(),
view_name='collection-detail',
read_only=True
)
# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
@api_view(['GET'])
def product_detail(request, pk):
product = Product.objects.select_related('collection').get(pk=pk)
serializer = ProductSerializer(product, context={'request': request})
return Response(serializer.data)
@api_view(['GET'])
def collection_detail(request, pk):
collection = get_object_or_404(Collection, pk=pk)
serializer = CollectionSerializer(collection)
return Response(serializer.data)
# urls.py
from django.urls import path
urlpatterns = [
path('collections/<int:pk>/', collection_detail, name='collection-detail'),
]

Nested serializers let you include the full details of a related object inside the serialized output, instead of just an ID or a link. For example, if you want to include the full details of the related Collection inside ProductSerializer, you can define a nested serializer for Collection and use it inside ProductSerializer:

class CollectionSerializer(serializers.Serializer):
id = serializers.IntegerField()
title = serializers.CharField(max_length=255)
class ProductSerializer(serializers.Serializer):
# other fields...
collection = CollectionSerializer()

Model serializers give you a shortcut for building serializers that work with model instances and querysets. They automatically create the fields for you based on the model, and can also include simple default versions of the create() and update() methods. Here is how you can create a model serializer for the Product model:

from rest_framework import serializers
from .models import Product
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ['id', 'title', 'unit_price', 'collection']

You can override fields inside a ModelSerializer to change how they behave. For example, if you want the collection field to use HyperlinkedRelatedField instead of the default primary key form, you can do it like this:

class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ['id', 'title', 'unit_price', 'collection']
collection = serializers.HyperlinkedRelatedField(
queryset=Collection.objects.all(),
view_name='collection-detail',
read_only=True
)

In a Model Serializer, if you override a field, the default version of that field gets ignored, and your overridden field is used instead. So in the example above, the collection field in ProductSerializer will use HyperlinkedRelatedField instead of the default primary key form that ModelSerializer would normally create.

You can also add custom fields to a ModelSerializer that are not directly tied to a model field. For example, if you want to add a price_with_tax field that calculates the price including tax, you can do it like this:

class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ['id', 'title', 'unit_price', 'price_with_tax', 'collection']
price_with_tax = serializers.SerializerMethodField()
def get_price_with_tax(self, obj):
# Assuming a tax rate of 10%
return obj.unit_price * Decimal(1.1)

If you want to include every field from the model in your serializer, you can use __all__ in the fields attribute inside the Meta class. This automatically includes every field from the model without you having to list them one by one. For example:

class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = '__all__'

When you use annotate() on a queryset, Django adds extra fields (like counts or sums) to each object on the fly. But since these fields are not part of the actual model, the serializer does not pick them up automatically.

To include an annotated field in a serializer:

  1. You need to declare the field directly in the serializer.
  2. You also need to add it to the fields list inside the Meta class.
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Collection
fields = ["id", "title", "products_count"]
products_count = serializers.IntegerField()
# views.py
from django.db.models import Count
@api_view(['GET'])
def collection_list(request):
collections = Collection.objects.annotate(products_count=Count('products'))
serializer = CollectionSerializer(collections, many=True)
return Response(serializer.data)

You can also use serializers to receive and check incoming data. For example, if you want to create a new product through a POST request, you can do it like this:

from rest_framework import serializers
from .models import Product
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ['id', 'title', 'unit_price', 'collection']
# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
@api_view(['POST'])
def create_product(request):
serializer = ProductSerializer(data=request.data)
if serializer.is_valid():
product = serializer.save()
return Response(
ProductSerializer(product).data,
status=status.HTTP_201_CREATED
)
else:
return Response(
serializer.errors,
status=status.HTTP_400_BAD_REQUEST
)

When you receive data through a serializer, you can check it using the is_valid() method. This method checks if the data matches the rules set on the serializer’s fields. If the data is valid, you can get the cleaned-up data through the validated_data property. For example:

# manual error handling
@api_view(['POST'])
def create_product(request):
serializer = ProductSerializer(data=request.data)
if serializer.is_valid():
validated_data = serializer.validated_data
return Response(
ProductSerializer(product).data,
status=status.HTTP_201_CREATED
)
else:
return Response(
serializer.errors,
status=status.HTTP_400_BAD_REQUEST
)
# automatic error handling
@api_view(['POST'])
def create_product(request):
serializer = ProductSerializer(data=request.data)
# This will raise a ValidationError if the data is invalid
serializer.is_valid(raise_exception=True)
product = serializer.save()
return Response(
ProductSerializer(product).data,
status=status.HTTP_201_CREATED
)

You can also add your own validation rules to a serializer by writing validate_<field_name> methods, or by overriding the validate method for checks that involve the whole object. For example, if you want to make sure unit_price is greater than zero, you can do it like this:

# field-level validation
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ['id', 'title', 'unit_price', 'collection']
def validate_unit_price(self, value):
if value <= 0:
raise serializers.ValidationError("Unit price must be greater than zero.")
return value
# object-level validation
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ['id', 'title', 'unit_price', 'collection']
def validate(self, data):
if data['unit_price'] <= 0:
raise serializers.ValidationError("Unit price must be greater than zero.")
return data

When you call the save() method on a serializer, it will either create a new instance or update an existing one, depending on whether the serializer was given an existing instance when it was created. The save() method internally calls either create() or update() on the serializer, and you can override these methods to customize how saving works. For example:

# views.py
@api_view(['POST'])
def create_product(request):
serializer = ProductSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
product = serializer.save()
return Response(
ProductSerializer(product).data,
status=status.HTTP_201_CREATED
)

When you want to update an existing instance, you create the serializer with that instance plus the new data. For example:

@api_view(["GET",'PUT'])
def update_product(request, pk):
product = get_object_or_404(Product, pk=pk)
if request.method == 'GET':
serializer = ProductSerializer(product)
return Response(serializer.data)
elif request.method == 'PUT':
serializer = ProductSerializer(product, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)

By default, ModelSerializer already comes with create() and update() methods that simply create or update a model instance using the validated data. But you can override these methods to add your own custom logic while creating or updating instances. For example:

class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ['id', 'title', 'unit_price', 'collection']
def create(self, validated_data):
# Custom logic for creating a product
product = Product(**validated_data)
product.field_name = 'custom value' # example of adding custom logic
product.save()
return product
def update(self, instance, validated_data):
# Custom logic for updating a product
# example of adding custom logic
instance.title = validated_data.get('title').strip()
instance.field_name = 'custom value'
instance.save()
return instance

To delete an instance, you call the delete() method directly on the instance you want to remove. For example:

@api_view(['DELETE'])
def delete_product(request, pk):
product = get_object_or_404(Product, pk=pk)
product.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

If you try to delete an object that has models.PROTECT or models.RESTRICT set as its on_delete behavior, it will raise a ProtectedError or RestrictedError. To handle this, you can catch the exception and return a proper response. For example:

from django.db.models import ProtectedError
from rest_framework import status
@api_view(['DELETE'])
def delete_product(request, pk):
product = get_object_or_404(Product, pk=pk)
try:
product.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except ProtectedError:
return Response(
data={"detail": "Cannot delete this product because it is protected."},
status=status.HTTP_400_BAD_REQUEST
)