Working with request.data in Django REST framework

Django REST generic views are amazing, but working with request.data in Django REST framework can be tricky ...

Working on request.data in Django REST framework

Django REST generic views are amazing. It's hard to justify writing a flow-complete view by hand unless you're doing something so easy that doesn't require validation or other stuff.

Even then why leaving the enlightened path? There are situations however where you want to change request.data a bit in a generic view, and things will get tricky ...

The problem: an example with CreateAPIView

CreateAPIView is a concrete view for handling the POST/return response lifecycle in a RESTful API. It accepts JSON POST requests.

After installing and configuring DRF all you need to start accepting requests is a subclass of CreateAPIView with a serializer. Example:

# library/views/api.py
from rest_framework.generics import CreateAPIView
from library.serializers import ContactSerializer


class ContactCreateAPI(CreateAPIView):
    serializer_class = ContactSerializer

Here ContactSerializer is a DRF model serializer for a simple model. Here's the serializer:

from rest_framework.serializers import ModelSerializer
from .models import Contact


class ContactSerializer(ModelSerializer):
    class Meta:
        model = Contact
        fields = ("first_name", "last_name", "message")

And here's the model:

from django.db import models


class Contact(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    message = models.TextField(max_length=400)

    def __str__(self):
        return f"{self.first_name} {self.last_name}"

It's all bells and whistles until the frontend sends an object with exactly the same properties found in the serializer.

What I mean is that before sending the POST request from Fetch you have to build this object:

const data = {
    first_name: "Juliana",
    last_name: "Crain",
    message: "That motel in Canon City though"
}

It's easy with a FormData if you have all the inputs with the appropriate name attributes. But, if you fail to do so DRF will respond with a 400 bad request. The solution? A bit of tweaking on the CreateAPIView subclass.

When we extend a Python class, here specifically CreateAPIView, we can also override inherited methods. If we snitch into the original CreateAPIView we can see a post method:

# Original CreateAPIView from DRF
class CreateAPIView(mixins.CreateModelMixin,
                    GenericAPIView):
    """
    Concrete view for creating a model instance.
    """
    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

Seems a good spot for changing the request ...

AttributeError: This QueryDict instance is immutable

When Django REST frameworks receives a request, request.data is the entry point for your ... data. The JSON payload from your frontend will end up there.

Let's imagine a colleague doesn't know the exact shape for the request object and instead of sending this:

const data = {
    first_name: "Juliana",
    last_name: "Crain",
    message: "That motel in Canon City though"
}

sends this:

const data = {
    name: "Juliana",
    surname: "Crain",
    message: "That motel in Canon City though"
}

Let's also say you replicated the error on three different frontend and there's no easy way to come back.

How can we transform this JSON object in request.data to avoid a 400? Easier done than said! Just override the post method and mess up with the data:

from rest_framework.generics import CreateAPIView
from library.serializers import ContactSerializer


class ContactCreateAPI(CreateAPIView):
    serializer_class = ContactSerializer

    def post(self, request, *args, **kwargs):
        if (name := request.data.get("name")) and (
            surname := request.data.get("surname")
        ):

            request.data["first_name"] = name
            request.data["last_name"] = surname
            return self.create(request, *args, **kwargs)
        return self.create(request, *args, **kwargs)

If only was that easy! If we run this view we get AttributeError: This QueryDict instance is immutable. Surprise!

request.data in fact is a Django QueryDict which turns out to be immutable.

The only way to change it is to copy the object and modify the copy. But there's no way to swap back request.data with your own object because at this stage request is immutable too.

So where do we intercept and swap request.data?

NOTE: if you want to test this view check out DRF: testing POST requests.

get_serializer to the rescue

When subclassing CreateAPIView we get access to all the methods defined in CreateModelMixin and GenericAPIView:

# Original CreateAPIView from DRF
class CreateAPIView(mixins.CreateModelMixin,
                    GenericAPIView):
    """
    Concrete view for creating a model instance.
    """
    ##

Here's the UML diagram from Pycharm:

CreateAPIView diagram

CreateModelMixin is pretty simple, with three methods: create, perform_create, get_success_headers.

create in particular is interesting because it forwards request.data to another method named get_serializer. Here's the relevant code:

# CreateModelMixin from DRF
class CreateModelMixin:
    """
    Create a model instance.
    """
    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)

    # There are two more methods here ... omitted

get_serializer is not found directly on CreateModelMixin, it lives on GenericAPIView:

# Original GenericAPIView from DRF
class GenericAPIView(views.APIView):
    def get_serializer(self, *args, **kwargs):
        """
        Return the serializer instance that should be used for validating and
        deserializing input, and for serializing output.
        """
        serializer_class = self.get_serializer_class()
        kwargs['context'] = self.get_serializer_context()
        return serializer_class(*args, **kwargs)

Bingo! What if we override this method in our view to intercept and change kwargs["data"]?

Intercepting request.data in the right place

In our view we can override get_serializer with our own version:

from rest_framework.generics import CreateAPIView
from library.serializers import ContactSerializer


class ContactCreateAPI(CreateAPIView):
    serializer_class = ContactSerializer

    def get_serializer(self, *args, **kwargs):
        # leave this intact
        serializer_class = self.get_serializer_class()
        kwargs["context"] = self.get_serializer_context()

        """
        Intercept the request and see if it needs tweaking
        """
        if (name := self.request.data.get("name")) and (
            surname := self.request.data.get("surname")
        ):

            #
            # Copy and manipulate the request
            draft_request_data = self.request.data.copy()
            draft_request_data["first_name"] = name
            draft_request_data["last_name"] = surname
            kwargs["data"] = draft_request_data
            return serializer_class(*args, **kwargs)
        """
        If not mind your own business and move on
        """
        return serializer_class(*args, **kwargs)

If request.data has wrong fields we make a copy, we modify the fields, and we place the copy on the data keyword argument:

# omit
draft_request_data = self.request.data.copy()
# omit
kwargs["data"] = draft_request_data

Now the serializer will receive the expected data shape and won't complain anymore. In case the fields are ok instead we go straight to the happy path.

NOTE: in the example I'm using the warlus operator from Python 3.8.

Wrapping up

The request object in Django REST framework is immutable and so request.data. To alter the payload we can make a copy, but there's no way to swap the original object with our copy, at least in a post method.

A custom override of get_serializer from the generic DRF view can solve the issue in a cleaner way.

Thanks for reading!

Resources

Valentino Gagliardi

Hi! I'm Valentino! I'm a freelance consultant with a wealth of experience in the IT industry. I spent the last years as a frontend consultant, providing advice and help, coaching and training on JavaScript, testing, and software development. Let's get in touch!

More from the blog: