Why is django-rest-frameworks request.data sometimes immutable?

Viewed 4714

In my restful CreateAPIView I mutate my request.data dictionary.

Occasionally I receive an error not caught by my tests:

This QueryDict instance is immutable

For e.g. this:

class CreateView(CreateAPIView):
    serializer_class = ...
    queryset = ...

    def post(self, request, *args, **kwargs):
        request.data['user'] = request.user.pk
        return self.create(request, *args, **kwargs)

request.data seems to be a normal dict in my tests. Why is it sometimes a QueryDict? How should this be dealt with? Should request.data not be mutated in general? How should you use the ModelSerializer class, when you need to populate some fields yourself?

4 Answers

Why this occasional behavior?

When we look into the SC of Request (as @Kenny Ackerman mentioned), it return a QueryDict object if you are passing a form media type ('application/x-www-form-urlencoded' or 'multipart/form-data') data to the view class.
This check being execute within the is_form_media_type() method of Request class.

If you are passing a application/json data to the view, the request.data will be a dict object.

How to reproduce the behaviour?

It can be reproduce by using sending different ContentType data into view. (In POSTMAN tool, use form-data and raw JSON to get the behaviour)


How to get current logged-in user in serializer?

Method-1 pass extra argument to .save() (as @Linovia mentioned) by overriding the perform_create() method

class CreateView(CreateAPIView):
    serializer_class = ...
    queryset = ...

    def post(self, request, *args, **kwargs):
        request.data['user'] = request.user.pk
        return self.create(request, *args, **kwargs)

    def perform_create(self, serializer):
        serializer.save(user=self.request.user)

Method-2 Use CurrentUserDefault() class as below

from django.contrib.auth import get_user_model

User = get_user_model()


class MySerializer(serializers.ModelSerializer):
    user = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), default=serializers.CurrentUserDefault())

    class Meta:
    # your code

based on the source code parser returns a querydict for data when the stream is empty(request.data calls _load_data_and_files method and _load_data_and_files calls _parse method).

and I think you can populate the fields using HiddenField or you can override the create or update method. for example

class TestSerializer(serializers.ModelSerializer):
    user = serializers.HiddenField(default=serializers.CurrentUserDefault())
    class Meta:
        model = Test
        fields = ('id', 'text', 'user')

    def create(self, validated_data):

        validated_data['populate_field'] = 'value'
        return super().create(validated_data)

When you have to modify a QueryDict object received from a request, it is a immutable object, instead use this line of code if you wanna add attributes:

myNewRequest = request.GET.copy()
myNewRequest.data['some_attr'] = float(something)

Note that this really depends on your selected parser specified in rest_framework's DEFAULT_PARSER_CLASSES and the content type of your request:

JSONParser is implemented as follows:

return json.load(decoded_stream, parse_constant=parse_constant)

FormParser as follows:

return QueryDict(stream.read(), encoding=encoding)
Related