Tidbits | Aug. 20, 2019

Using Different Read and Write Serializers in Django REST Framework

by Lacey Williams Henschel |   More posts by Lacey

Versions:

  • Python 3.7
  • Django 2.2
  • Django REST Framework 3.10

On a recent project, we needed to use different serializers for GET vs. POST/PUT/PATCH requests to our Django REST Framework API. In our case, this was because the GET serializer contained a lot of nested data; for example, it contained expanded fields from other serializers to foreign-key relationships. The requests to update data via the API, though, didn't need these expanded fields.

The first way we approached using different serializers for read and update actions was to override get_serializer_class() on each viewset to decide which serializer to return depending on the action in the request. We returned the "read" serializer for list and retrieve actions, and the "update" serializer for everything else. (The full list of API actions is in the DRF codebase.) But we wound up repeating ourselves across several viewsets, so we wrote a mixin to take care of some of this work for us!

A mixin is a Python class that contains custom attributes and methods (more explanation). It's not very useful on its own, but when it's inherited into a class, that class has access to the mixin's special attributes and methods.

This was our mixin:

class ReadWriteSerializerMixin(object):
    """
    Overrides get_serializer_class to choose the read serializer
    for GET requests and the write serializer for POST requests.

    Set read_serializer_class and write_serializer_class attributes on a
    viewset. 
    """

    read_serializer_class = None
    write_serializer_class = None

    def get_serializer_class(self):        
        if self.action in ["create", "update", "partial_update", "destroy"]:
            return self.get_write_serializer_class()
        return self.get_read_serializer_class()

    def get_read_serializer_class(self):
        assert self.read_serializer_class is not None, (
            "'%s' should either include a `read_serializer_class` attribute,"
            "or override the `get_read_serializer_class()` method."
            % self.__class__.__name__
        )
        return self.read_serializer_class

    def get_write_serializer_class(self):
        assert self.write_serializer_class is not None, (
            "'%s' should either include a `write_serializer_class` attribute,"
            "or override the `get_write_serializer_class()` method."
            % self.__class__.__name__
        )
        return self.write_serializer_class

This mixin defines two new attributes, read_serializer_class and write_serializer_class. Each attribute has a corresponding method to catch the error where the mixin is being used, but those attributes haven't been set. The get_*_serializer_class() methods will raise an AssertionError if your viewset hasn't set the appropriate attribute or overridden the necessary method.

The get_serializer_class method makes the final decision on which serializer to use. For the "update" actions to the API, it returns write_serializer_class; otherwise it returns read_serializer_class.

The mixin gets used in a viewset like this:

from rest_framework import viewsets

from .mixins import ReadWriteSerializerMixin
from .models import MyModel
from .serializers import ModelReadSerializer, ModelWriteSerializer


class MyModelViewSet(ReadWriteSerializerMixin, viewsets.ModelViewSet):
    queryset = MyModel.objects.all() 
    read_serializer_class = ModelReadSerializer 
    write_serializer_class = ModelWriteSerializer

Now the viewset MyModelViewSet has access to the attributes and methods from the mixin ReadWriteSerializerMixin. This means that when a call is made to the API that uses MyModelViewSet, the get_serializer_class() method from ReadWriteSerializerMixin will automatically be called and will decide, based on the kind of API request being made, which serializer to use. If we needed to make even more granular decisions about the serializer returned (maybe we want to use a more limited serializer for a list request and one with more data in a retrieve request), then our viewset can override get_write_serializer_class() to add that logic.

Note: Custom DRF actions will contain actions that aren't part of the DRF list of accepted actions (because they are custom actions you're creating), so when you call get_serializer_class from inside your action method, it will return whatever your "default" serializer class is. In the example above, the "default" serializer is the read_serializer_class because it's what we return when we fall through the other conditional.

Depending on your action, you will want to override get_serializer_class to change your default method or explicitly account for your custom action.

Mixins are a DRY (Don't Repeat Yourself) way to add functionality that you wind up needing to use across several viewsets. We hope you get to experiment with using them soon!

Thanks to Jeff Triplett for his help with this post.


drf   django  

On a recent project, we needed to use different serializers for GET vs POST/PUT/PATCH requests to our API. Read on to learn how we used a mixin to accomplish this goal.{% else %}On a recent project, we needed to use different serializers for GET vs POST/PUT/PATCH requests to our API. Read on to learn how we used a mixin to accomplish this goal.

2019-08-20T15:47:00 2019-08-20T16:15:43.591989 2019 drf,django