Python – Django Rest Framework – Updating related model using ModelSerializer and ModelViewSet

djangodjango-rest-frameworkpython

BACKGROUND

I have two serializers: PostSerializer and PostImageSerializer which both inherit DRF ModelSerializer. The PostImage model is linked with Post by related_name='photos'.

Since I want the serializer to perform update, PostSerializer overrides update() method from ModelSerializer as stated in official DRF doc.

class PostSerializer(serializers.ModelSerializer):
    photos = PostImageSerializer(many=True)

    class Meta:
        model = Post
        fields = ('title', 'content')

    def update(self, instance, validated_data):
        photos_data = validated_data.pop('photos')
        for photo in photos_data:
            PostImage.objects.create(post=instance, image=photo)
        return super(PostSerializer, self).update(instance, validated_data)

class PostImageSerializer(serializer.ModelSerializer):
    class Meta:
        model = PostImage
        fields = ('image', 'post')

I have also defined a ViewSet which inherits ModelViewSet.

 class PostViewSet(viewsets.ModelViewSet):
        queryset = Post.objects.all()
        serializer_class = PostSerializer

Finally the PostViewSet is registered to DefaultRouter. (Omitted code)

Goal

The goals are simple.

Problem

I'm getting 400 Response with error message as following.

{
"photos": [
"This field is required."
],
"title": [
"This field is required."
],
"content": [
"This field is required."
]
}

(Should you plz note that the error messages might not exactly fit with DRF error messages since they are translated.)

It is obvious that none of my PUT fields are applied.
So I have been digging around Django rest framework source code itself and found out serializer validation in ViewSet update() method continues to fail.

I doubt that because I PUT request not by JSON but by form-data using key-value pair so request.data is not properly validated.

However, I should contain multiple images in the request which means plain JSON would not work.

What would be the most clear solutions for this case?

Thank you.

Update

As Neil pointed out, I added print(self) at the first line of update() method of PostSerializer. However nothing printed out on my console.

I think this is due to my doupt above because perform_update() method which calls serializer update() method is called AFTER serializer is validated.

Therefore the main concept of my question could be narrowed to the followings.

  1. How should I fix requested data fields so that validation inside update() method of ModelViewSet could pass?
  2. Do I have to override update() method of ModelViewSet(not the one from ModelSerializer)?

Thanks again.

Best Answer

First of all you need to set header:

Content-Type: multipart/form-data;

But maybe if you set form-data in postman, this header should be default.

You can't send images as a json data (unless you encode it to string and decode on server side to image eg. base64).

In DRF PUT by default requires all fields. If you want to set only partial fields you need to use PATCH.

To get around this and use PUT to update partial fields you have two options:

  • edit update method in viewset to partial update serializer
  • edit router to always call partial_update method in serializers which is more advanced

You can override viewset update method to always update serializer partial (changing only provided fields):

    def update(self, request, *args, **kwargs):
        partial = True # Here I change partial to True
        instance = self.get_object()
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)
        self.perform_update(serializer)

        return Response(serializer.data)

Add

rest_framework.parsers.MultiPartParser

to the main settings file to the REST_FRAMEWORK dict:

REST_FRAMEWORK = {
    ...
    'DEFAULT_PARSER_CLASSES': (
        'rest_framework.parsers.JSONParser',
        'rest_framework.parsers.MultiPartParser',
    )
}

Looking at your serializers it's weird that you don't get error from PostSerializer because you don't add "photos" field to Meta.fields tuple.

More advices from me in this case:

  • add required=False to your photos field (unless you want this to be required)
  • as wrote above add photos field to you Meta.fields tuple fields = ('title', 'content', 'photos',)
  • add default None value for your validated_data.pop('photos'), then check photos data is provided before loop.
Related Topic