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 ...
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:
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!