Django REST Framework Read & Write Serializers
Hugo Bessa • 6 March 2018
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') @property 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: instance.meals.add(meal) 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_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(data=request.data) write_serializer.is_valid(raise_exception=True) instance = self.perform_create(write_serializer) read_serializer = OrderListSerializer(instance) return Response(read_serializer.data)
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
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 (
get_write_serializer_class and it can also be overridden the same way we did previously with
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.