Django REST Framework Read & Write Serializers

Django REST Framework (DRF) is a terrific tool for creating very flexible REST APIs. It has a lot of built-in features like pagination, search, filters, throttling, and many other things developers usually don't like to worry about. And it also lets you easily customize everything so you can make your API work the way you want.

There are many generic classes you can use to implement your API endpoints, some with more or fewer abstraction. Check Classy Django REST Framework to find the best class to use in each situation and understand how to override their behavior.

One family of classes DRF has is Serializers. They're used to convert the data sent in a HTTP request to a Django object and a Django object to a valid response data. It looks a lot like a Django Form, but it is also concerned in defining how the data will be returned to the user. There's also the ModelSerializer, which works like a ModelForm: it uses one of your models as a basis to create its fields and its validation.

Serializers use the same block of code for defining how the data will be interpreted when it comes from a request and how it will look like in a response. But handling both the input and output with the same code is not ideal. some cases, you probably will need more than one serializer for the same endpoint. That's why we made the drf-rw-serializers project: a set of generic views, viewsets and mixins that extend DRF to have different serializers for read and write operations.

Let's look at an example where this is the case. We'll build an API for a Cafe:

  • We'll need an endpoint that creates orders in the cafe;
  • Orders have a table number and a list of meals;
  • A meal has a title and a price;
  • This endpoint must have:
  • a GET method for listing the orders and;
  • a POST method to create a new order.
  • In the GET requests, I want to receive a list of all the orders and, for each order, I want to see every meal requested;
  • In the GET requests, I also want to have the total price of each order;
  • In the POST requests, since meals are pre-registered in the system, I want to send only the meals ids instead of the whole meal object.

Let's consider these models:

from django.db import models

class Order(models.Model):
    table_number = models.IntegerField()
    meals = models.ManyToManyField('meals')

    def total_price(self):
        qs = self.meals.through.objects.all().aggregate(total_price=models.Sum('meal__price'))
        return qs['total_price']

class Meal(models.Model):
    name = models.CharField(max_length=100)
    price = models.DecimalField(decimal_places=2, max_digits=5)

For creating meals we need an OrderSerializer. The following example should be enough:

from rest_framework import serializers
from .models import Order

class OrderCreateSerializer(serializers.ModelSerializer):

    class Meta:
        model = Order
        fields = ('id', 'table_number', 'meals')

    def create(self, validated_data):
        meals = validated_data.pop('meals')
        instance = Order.objects.create(**validated_data)
        for meal in meals:

        return instance

But when you try to reuse these serializers for the list operation you'll find out that they won't be enough. This happens because they don't contain the meals in the orders, only their ids. To fulfill a list operation we would need two other serializers:

from rest_framework import serializers
from .models import Order, OrderedMeal, Meal

class MealSerializer(serializers.ModelSerializer):

    class Meta:
        model = Meal
        fields = ('id', 'name', 'price')

class OrderListSerializer(serializers.ModelSerializer):
    ordered_meals = MealSerializer(many=True)

    class Meta:
        model = Order
        fields = ('id', 'table_number', 'meals', 'total_price')

In a case like this, we can't reuse the same serializer for doing both list and create, unless you override lower level methods like to_internal_value and to_representation, but this way you'd lose almost the whole abstraction ModelSerializer class gives you.

Another point for using two serializers instead of making workarounds to use only one is that the read logic and the write logic will probably evolve differently in a use case like this. So if the logic is so decoupled why to share lines of code?

This is one of the few cases where we shouldn't blindly follow the "Don't Repeat Yourself" philosophy. It's better to repeat lines of code that are known to have different concerns and will evolve separately. By doing this, we avoid messy workarounds to keep the code in one place.

However, DRF is not so friendly when you need to have multiple serializers in different methods of the same view. When you have write operations like create or update in your endpoints, you usually expect them to return the created/updated object. But not with the exact same data you sent: you want the data to look like the one users fetch from the API, with nested details and maybe some processed data.

Therefore, it's better to use one serializer to save the data sent in the request and another to return the created/updated resource representation.

To use DRF's wonderful generic API views you'd have to override some of their methods to implement this logic. For our Cafe API the implementation of the view would be something like this:

from rest_framework import generics
from .models import Orders
from .serializers import OrderCreateSerializer, OrderListSerializer

class OrderListCreateView(generics.ListCreateAPIView):
    queryset = Order.objects.all()
    serializer_class = OrderListSerializer

    def create(self, request, *args, **kwargs):
        write_serializer = OrderCreateSerializer(
        instance = self.perform_create(write_serializer)

        read_serializer = OrderListSerializer(instance)
        return Response(

To avoid this, we at Vinta created generic classes similar to DRF ones to have separate input/output serializers. With our package, drf-rw-serializers, the OrderCreateView, would be like this:

from drf_rw_serializers import generics
from .models import Orders
from .serializers import OrderCreateSerializer, OrderListSerializer

class OrderListCreateView(generics.ListCreateAPIView):
    queryset = Order.objects.all()
    write_serializer_class = OrderCreateSerializer
    read_serializer_class = OrderListSerializer

Also if you need a different serializer for different methods you can override similar methods as the get_serializer_class from DRF generics.GenericAPIView. For instance, suppose that we want to use another serializer called OrderDetailSerializer when we're getting the response of create method. Our View would be like this:

from drf_rw_serializers import generics
from .models import Orders
from .serializers import (
    OrderCreateSerializer, OrderListSerializer, OrderDetailSerializer)

class OrderListCreateView(generics.ListCreateAPIView):
    queryset = Order.objects.all()
    write_serializer_class = OrderCreateSerializer
    def get_read_serializer_class(self):
        if self.request.method == 'POST':
            return OrderDetailSerializer
        return OrderListSerializer

Overriding get_read_serializer_class method is also useful if you want to check other condition like the user role or something in your app's context to choose the proper serializer for the read operations. As well as the method for retrieving the read operations serializer, we also made available a similar method for selecting the one for write operations (write_serializer_class) called get_write_serializer_class and it can also be overridden the same way we did previously with get_read_serializer_class.

drf-rw-serializers is open source, and its available on github and on PyPI. You can also read the full instruction of how to use it in its documentation.

We've been using an implementation like this for a while in Vinta's projects, but this is a brand new package so if you have any suggestions, questions or problems using it feel free to create issues on the github repository. Also, contributions are welcome.

Special thanks to Flávio Juvenal and Rebeca Sarai and Tulio Lages for reviewing this post.

Hugo Bessa

Tech Lead at Vinta Software.