- 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
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,
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.
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
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
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.