petroleyum petroleyum - 3 months ago 10
Python Question

How to post/put json data to ListSerializer

I'm reading about customizing multiple update here and I haven't figured out in what case the custom

ListSerializer
update method is called. I would like to update multiple objects at once, I'm not worried about multiple create or delete at the moment.

From the example in the docs:

# serializers.py
class BookListSerializer(serializers.ListSerializer):
def update(self, instance, validated_data):
# custom update logic
...

class BookSerializer(serializers.Serializer):
...
class Meta:
list_serializer_class = BookListSerializer


And my ViewSet

# api.py
class BookViewSet(ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer


And my url setup using
DefaultRouter


# urls.py
router = routers.DefaultRouter()
router.register(r'Book', BookViewSet)

urlpatterns = patterns('',
url(r'^api/', include(router.urls)),
...


So I have this set up using the
DefaultRouter
so that
/api/Book/
will use the
BookSerializer
.

Is the general idea that if I POST/PUT/PATCH an array of JSON objects to
/api/Book/
then the serializer should switch over to
BookListSerializer
?

I've tried POST/PUT/PATCH JSON data list to this
/api/Book/
that looks like:

[ {id:1,title:thing1}, {id:2, title:thing2} ]


but it seems to still treat the data using
BookSerializer
instead of
BookListSerializer
. If I submit via POST I get
Invalid data. Expected a dictionary, but got list
. and if I submit via PATCH or PUT then I get a
Method 'PATCH' not allowed
error.

Question:
Do I have to adjust the
allowed_methods
of the
DefaultRouter
or the
BookViewSet
to allow POST/PATCH/PUT of lists? Are the generic views not set up to work with the
ListSerializer
?

I know I could write my own list deserializer for this, but I'm trying to stay up to date with the new features in DRF 3 and it looks like this should work but I'm just missing some convention or some option.

Answer

Django REST framework by default assumes that you are not dealing with bulk data creation, updates, or deletion. This is because 99% of people are not dealing with bulk data creation, and DRF leaves the other 1% to third-party libraries.

In Django REST framework 2.x and 3.x, a third party package exists for this.

Now, you are trying to do bulk creation but you are getting an error back that says

Invalid data. Expected a dictionary, but got list

This is because you are sending in a list of objects to create, instead of just sending in one. You can get around this a few ways, but the easiest is to just override get_serializer on your view to add the many=True flag to the serializer when it is a list.

def get_serializer(self, *args, **kwargs):
    if "data" in kwargs:
        data = kwargs["data"]

        if isinstance(data, list):
            kwargs["many"] = True

    return super(MyViewSet, self).get_serializer(*args, **kwargs)

This will allow Django REST framework to know to automatically use the ListSerializer when creating objects in bulk. Now, for other operations such as updating and deleting, you are going to need to override the default routes. I'm going to assume that you are using the routes provided by Django REST framework bulk, but you are free to use whatever method names you want.

You are going to need to add methods for bulk PUT and PATCH to the view as well.

from rest_framework.response import Response

def bulk_update(self, request, *args, **kwargs):
    partial = kwargs.pop("partial", False)

    queryset = self.filter_queryset(self.get_queryset))

    serializer = self.get_serializer(instance=queryset, data=request.data, many=True)
    serializer.is_valid(raise_exception=True)

    self.perform_update(serializer)

    return Response(serializer.data)

def partial_bulk_update(self, *args, **kwargs):
    kargs["partial"] = True
    return super(MyView, self).bulk_update(*args, **kwargs)

This won't work out of the box as Django REST framework doesn't support bulk updates by default. This means you also have to implement your own bulk updates. The current code will handle bulk updates as though you are trying to update the entire list, which is how the old bulk updating package previously worked.

While you didn't ask for bulk deletion, that wouldn't be particularly difficult to do.

def bulk_delete(self, request, *args, **kwargs):
    queryset = self.filter_queryset(self.get_queryset())
    self.perform_delete(queryset)
    return Response(status=204)

This has the same effect of removing all objects, the same as the old bulk plugin.

None of this code was tested. If it doesn't work, consider it as a detailed example.

Comments