25

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.

Kevin Brown-Silva
  • 40,873
  • 40
  • 203
  • 237
petroleyum
  • 678
  • 1
  • 6
  • 11

1 Answers1

44

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.

miki725
  • 27,207
  • 17
  • 105
  • 121
Kevin Brown-Silva
  • 40,873
  • 40
  • 203
  • 237
  • Thanks. Fixed that. Now I'm submitting a list to `/api/Book/`. 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.`. I'm using the `DefaultRouter` and a `ModelViewSet` if that helps. – petroleyum Jan 10 '15 at 21:33
  • 1
    I've updated the answer to address your question **in detail**. Hopefully it covers the multiple parts to your question. With any luck, the third party package will be updated to support 3.0 in the near future. – Kevin Brown-Silva Jan 11 '15 at 01:19
  • Awesome, thank you. I guess my question could have been boiled down to "It looks like DRF has some default bulk update behavior now with 3.0?" and the answer is "Not quite." Your code example is exactly what I needed, I'll test it out and update if necessary. – petroleyum Jan 11 '15 at 14:24
  • Thanks a bunch for this Kevin - this fixes things for us here: https://github.com/burke-software/django-sis/commit/7862c4d27f140411ed089e8d2ca39d9c0f3c7050 – Quentin Donnellan Jan 13 '15 at 03:53
  • Updated the answer that DRF-bulk now support both DRF2 and DRF3! - https://github.com/miki725/django-rest-framework-bulk – miki725 Feb 09 '15 at 11:09
  • DRF-bulk is not maintained anymore. What's current canonical solution for this? – Sassan Feb 27 '17 at 02:10
  • @sassan up until 3.4.x it worked fine, you may want to consider creating a ticket with them about any recent issues – Kevin Brown-Silva Feb 27 '17 at 02:11
  • Didn't try it when I saw last commit is 2 years ago. Now I'll try it. Thanks for quick response. – Sassan Feb 27 '17 at 02:12
  • I found a solution that's less complicated (at least for me as a Django beginner) https://stackoverflow.com/a/59756993/7392069 – Greg Holst Jan 15 '20 at 17:53
  • Than you for the excelent explanation! I'd wish to vote twice on it! – cavalcantelucas Jul 23 '21 at 10:19