What You Should Know About DRF, Part 2: Customizing built-in methods

This is Part 2 of a 3-part series on Django REST Framework viewsets. Read Part 1: ModelViewSet attributes and methods and Part 3: Adding custom endpoints.

I gave this talk at PyCascades 2021 and decided to turn it into a series of blog posts so it's available to folks who didn't attend the conference or don't like to watch videos. Here are the slides and the video if you want to see them.


If you came here from Part 1 of What You Should Know About Django REST Framework, you may be wondering why I just walked you through a bunch of source code.

We stepped through that code because if you know what the main methods of the ModelViewSet do and how they work, you know where to go when you want to tweak the behavior of your viewset. You can pull out the method that contains what you want to change, override it with your own custom behavior, and put it back in.

In Part 1, we were writing a BookViewSet. So let's go through a few cases where we might want to customize the behavior of our endpoints and walk through how we would do that.

How do I return different serializers for list and detail endpoints?

When I hit GET /books/ (so I'm seeing a list of books), I only want some of the book data. Maybe I want the cover image, the title, the author, and whether there are books available. For this, I want to use my BookListSerializer.

But when I hit GET /books/{id}/ (so I'm on the page for a specific book), I want all that data and more. I want links to other books the author has written, reviews for the book, the number of copies, and the year it was published. For this data, I want to use my BookDetailSerializer.

Remember the method that DRF uses to return the serializer class, get_serializer_class()? That's the method we want to override.

from rest_framework.permissions import AllowAny
from rest_framework.viewsets import ModelViewSet 

from .models import Book
from .serializers import BookDetailSerializer, BookListSerializer

class BookViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookDetailSerializer
    permission_classes = [AllowAny]

    def get_serializer_class(self):
        if self.action in ["list"]:
            return BookListSerializer
        return super().get_serializer_class()

In line 5, we import both serializers.

Then on line 9, we set the serializer_class attribute to whichever serializer we want to be the default. I'm going to set the default serializer to BookDetailSerializer, because there is only one case where I want to use the list serializer.

Then, I create my own get_serializer_class() method.

The self.action attribute is set by the the DRF ModelViewSet and is set to the name of the request method (see the source code). Remember that ModelViewSet doesn't use HTTP methods like get() and post(). It uses action-based method names, so the /books/ list endpoint uses the list() method.

Since we know this, we can check the value of self.action to decide which serializer to return.

if self.action in ["list"]:
    return BookListSerializer

If action is "something from this list", then we return the list serializer. The only thing in the list is "list", but I like the if value in [list of things] syntax so I can add more actions later if I need to.

If the action is not in the list of actions we define, then we want DRF to return whatever it was going to if we hadn't done anything. To do that, we call super():

if self.action in ["list"]:
    return BookListSerializer
return super().get_serializer_class()

How do I create things with one serializer, but return them with another serializer?

Our API includes an endpoint that allows the creation of new books. Maybe our serializer performs some post-processing and we want to be able to return the results of that post-processing in the response using a different serializer than the one we use to create the book.

This is another common use case where you want to use more than one serializer, but in this case, it would be much harder to accomplish this by just overriding get_serializer_class() because you want to change serializers while you're performing your action.

The easiest way to do this is by overriding the action method itself. First, let's review the create() and perform_create() methods from CreateModelMixin:

class CreateModelMixin:
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(
            serializer.data, status=status.HTTP_201_CREATED, headers=headers
        )

    def perform_create(self, serializer):
        serializer.save()

The create() method of the ModelViewSet does a few things:

  1. Retrieves the serializer using self.get_serializer() and passes in the data from the request
  2. Checks that the serializer is valid, and raises an error if it isn't
  3. Calls perform_create() with the serializer, which calls save() on the serializer but doesn't return anything
  4. Calls get_success_headers()
  5. Returns the serializer data with the Response object

What we want to do is start with one serializer (first line of the create() method), but after we've created our instance (by calling perform_create()), we want to switch to a different serializer.

For this to work, we need to be able to access the instance we just created. Luckily, the instance is returned from the serializer's save() method -- it's just that RF's perform_create() method doesn't use it.

We can override the create() method and replace the line that calls perform_create().

from .serializers import BookSerializer, BookCreatedSerializer

class BookViewSet(ModelViewSet):
    ...

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        instance = serializer.save()
        return_serializer = BookCreatedSerializer(instance)
        headers = self.get_success_headers(return_serializer.data)
        return Response(
            return_serializer.data, status=status.HTTP_201_CREATED, headers=headers
        )

In the code above, I've basically copied DRF's create() method into my own BookViewSet. My create() method and theirs are almost identical. But I've replaced where DRF calls perform_create() with my own call to serializer.save() so I can save the instance that method returns in my own variable.

Then, I can instantiate my BookCreatedSerializer with the new book instance (and give this serializer the variable return_serializer), call get_success_headers(), and return the return_serializer data in the Response.

How can I remove endpoints from ModelViewSet?

Like we talked about in Part 1, using ModelViewSet gives you 6 endpoints from 5 mixins:

  • CreateModelMixin gives you POST /books/
  • RetrieveModelMixin gives you GET /books/{id}/
  • UpdateModelMixin gives you PUT /books/{id}/ and PATCH /books/{id}/ (full update and partial update)
  • ListModelMixin gives you GET /books/
  • DestroyModelMixin gives you DELETE /books/{id}/

But what if you don't need all those endpoints? Maybe you want your API to include the ability to perform all these actions except deleting books.

In that case, you can create your own ModelViewSet using only the mixins that give you the endpoints you want. To do everything but delete, your viewset would look like this:

from rest_framework.generics import GenericAPIView 
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin,
    UpdateModelMixin, ListModelMixin

# import models and serializers...

class BookViewSet(
    CreateModelMixin,
    RetrieveModelMixin,
    UpdateModelMixin,
    ListModelMixin,
    GenericAPIView
):
    ...

But DRF is smart! It knows that you might want to use only a few endpoints at a time, so DRF includes several convenience classes for you with different combinations of the GenericAPIView and the action mixins. You can see them on the ClassyDRF website under the Generics heading.

  • CreateAPIView = GenericAPIView + CreateModelMixin
  • ListAPIView = GenericAPIView + ListModelMixin
  • DestroyAPIView = GenericAPIView + DestroyModelMixin
  • UpdateAPIView = GenericAPIView + UpdateModelMixin
  • RetrieveAPIView = GenericAPIView + RetrieveModelMixin
  • ListCreateAPIView
  • RetrieveDestoryAPIView
  • RetrieveUpdateAPIView
  • RetrieveUpdateDestroyAPIView

You can use any of these convenience view classes to create the set of API endpoints you need for your project, or use the GenericAPIView class plus the mixins you need to create your own.


In Part 1: ModelViewSet attributes and methods, I covered the attributes and methods that ship with ModelViewSet, what they do, and why you need to know about them.

In Part 3: Adding custom endpoints, I tell you how to add your own custom endpoints to your viewset without having to write a whole new view or add anything new to your urls.py.