100

I would like to save and update multiple instances using the Django Rest Framework with one API call. For example, let's say I have a "Classroom" model that can have multiple "Teachers". If I wanted to create multiple teachers and later update all of their classroom numbers how would I do that? Do I have to make an API call for each teacher?

I know currently we can't save nested models, but I would like to know if we can save it at the teacher level. Thanks!

Chaz
  • 3,232
  • 5
  • 19
  • 12
  • Here is similar question with solution that worked for me: http://stackoverflow.com/questions/21439672/django-rest-framework-batch-create/31415417#31415417 – Marcin Rapacz Jul 14 '15 at 19:22

9 Answers9

93

I know this was asked a while ago now but I found it whilst trying to figure this out myself.

It turns out if you pass many=True when instantiating the serializer class for a model, it can then accept multiple objects.

This is mentioned here in the django rest framework docs

For my case, my view looked like this:

class ThingViewSet(viewsets.ModelViewSet):
    """This view provides list, detail, create, retrieve, update
    and destroy actions for Things."""
    model = Thing
    serializer_class = ThingSerializer

I didn't really want to go writing a load of boilerplate just to have direct control over the instantiation of the serializer and pass many=True, so in my serializer class I override the __init__ instead:

class ThingSerializer(serializers.ModelSerializer):
    def __init__(self, *args, **kwargs):
        many = kwargs.pop('many', True)
        super(ThingSerializer, self).__init__(many=many, *args, **kwargs)

    class Meta:
        model = Thing
        fields = ('loads', 'of', 'fields', )

Posting data to the list URL for this view in the format:

[
    {'loads':'foo','of':'bar','fields':'buzz'},
    {'loads':'fizz','of':'bazz','fields':'errrrm'}
]

Created two resources with those details. Which was nice.

Tom Manterfield
  • 6,515
  • 6
  • 36
  • 52
  • Ha, that's a good catch. I've updated my code to actually do something with the now defaulted many value. It was a typing error on my part. Turns out just sending the data in the format shown does the job via the deprecated method though. Warning, changes are untested. – Tom Manterfield Dec 31 '13 at 14:24
  • 1
    What does request.DATA look like in this case? It can't be a dictionary - or do they stick it in the dict somehow? – akaphenom Mar 06 '14 at 21:46
  • @akaphenom I don't know if you found your answer but it seems that request.DATA can be either a list containing a dict or a dict containing a list containing a dict depending on how you serialize it. At least that's been my experience. – whoisearth Oct 02 '14 at 12:15
  • its good to know. I have moved on from django work,so I haven't focused. But I ma happy to have this answer a little more complete. – akaphenom Oct 02 '14 at 13:34
  • @Yorkot can you help how to do a save and an update for a single post request using modelviewset? – Shift 'n Tab Dec 06 '16 at 16:29
  • Updated url mentioned in this response: http://www.django-rest-framework.org/api-guide/serializers/#dealing-with-multiple-objects – Freddie Jan 11 '17 at 18:32
  • 30
    doesn't work for me { "non_field_errors": [ "Invalid data. Expected a dictionary, but got list." ] } – rluts Sep 26 '18 at 13:55
  • How about the object include file - and send rquest with fromdata type? – seuling Feb 20 '19 at 16:36
  • doesn't solve my problem "Incorrect type. Expected pk value, received str." my serializer has this field here which won't accept two PKeys whatever i send ```ingredients = serializers.PrimaryKeyRelatedField( many=True, queryset=Ingredient.objects.all())``` – Panagiss May 09 '21 at 12:03
75

I came to a similar conclusion as Daniel Albarral, but here's a more succinct solution:

class CreateListModelMixin(object):

    def get_serializer(self, *args, **kwargs):
        """ if an array is passed, set serializer to many """
        if isinstance(kwargs.get('data', {}), list):
            kwargs['many'] = True
        return super(CreateListModelMixin, self).get_serializer(*args, **kwargs)
Roger Collins
  • 1,188
  • 1
  • 8
  • 12
  • 3
    This made my day! I confirm this works fine and accepts both lists and dict. – alekwisnia Mar 17 '17 at 16:01
  • 1
    How is this expected to work given request.data is a QueryDict and not a dict or list? It works in unit tests but not in actual runtime due to that fact (for me, at least). – alexdlaird Dec 24 '17 at 02:31
  • kwargs.get('data', {}) will return a QueryDict, and thus fail the ininstance, so many will not be set to True. – Roger Collins Dec 26 '17 at 19:01
  • 2
    @RogerCollins If one of the list items raises validation error, entire request fails. Is there a way to skip invalid items and create rest of the instances? – pnhegde Jul 03 '18 at 11:09
  • @pnhegde You'd have to include that logic in your serializer.You'd also have a lot of work making sure your front-end updated your model with the results, since the relations would be out of sync. – Roger Collins Jul 09 '18 at 16:35
61

Here's another solution, you don't need to override your serializers __init__ method. Just override your view's (ModelViewSet) 'create' method. Notice many=isinstance(request.data,list). Here many=True when you send an array of objects to create, and False when you send just the one. This way, you can save both an item and a list!

from rest_framework import status, viewsets
from rest_framework.response import Response

class ThingViewSet(viewsets.ModelViewSet):

"""This view snippet provides both list and item create functionality."""

    #I took the liberty to change the model to queryset
    queryset = Thing.objects.all()
    serializer_class = ThingSerializer

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data, many=isinstance(request.data,list))
        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)
SirSaleh
  • 1,452
  • 3
  • 23
  • 39
waqmax
  • 611
  • 5
  • 6
  • 4
    This answer seems more straight forward and how I would expect this functionality to be implemented. – Pitt Apr 21 '18 at 18:55
  • 2
    This one works and the top voted answer doesn't work for me. – darcyy Nov 05 '19 at 08:23
  • You can additionally add a `transaction.atomic()` block to make sure all elements are added – Felipe Buccioni Apr 01 '20 at 23:49
  • This deserves more vote as this was the only one that worked out for me and it's pretty straightforward too. – Steven Apr 28 '20 at 07:17
  • 1
    I ran into the: `Expected a dictionary, but got list.` error in the accepted answer and this one fixed it for me. Thanks. – Lewis Menelaws Jun 10 '20 at 18:46
  • Whenever I use `many=isinstance(request.data,list)` I get attribute error: `Got AttributeError when attempting to get a value for field 'user' on serializer FileUploadSerializer. The serializer field might be named incorrectly and not match any attribute or key on the list instance. Original exception text was: 'list' object has no attribute 'user'.` Error goes away if I use `many=True` instead. – haccks Oct 30 '20 at 20:56
  • Could anyone tell me how to do the same with the update multiple records using: `def update(self, request, *args, **kwargs): instance = self.get_object() ....... self.perform_update(serializer) return Response(serializer.data)` – Alex Jul 10 '21 at 04:32
  • I was working on similar problem, but in my case, I have to create multiple objects but some data might throw some error, so for many=true how can we do multiple partial update , i.e let say 6/10 got inserted – ratnesh Jul 15 '21 at 11:04
14

I couldn't quite figure out getting the request.DATA to convert from a dictionary to an array - which was a limit on my ability to Tom Manterfield's solution to work. Here is my solution:

class ThingSerializer(serializers.ModelSerializer):
    def __init__(self, *args, **kwargs):
        many = kwargs.pop('many', True)
        super(ThingSerializer, self).__init__(many=many, *args, **kwargs)

    class Meta:
        model = Thing
        fields = ('loads', 'of', 'fields', )

class ThingViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet ):
    queryset = myModels\
        .Thing\
        .objects\
        .all()
    serializer_class = ThingSerializer

    def create(self, request, *args, **kwargs):
        self.user = request.user
        listOfThings = request.DATA['things']

        serializer = self.get_serializer(data=listOfThings, files=request.FILES, many=True)
        if serializer.is_valid():
            serializer.save()
            headers = self.get_success_headers(serializer.data)
            return Response(serializer.data, status=status.HTTP_201_CREATED,
                            headers=headers)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

And then I run the equivalent of this on the client:

var things = {    
    "things":[
        {'loads':'foo','of':'bar','fields':'buzz'},
        {'loads':'fizz','of':'bazz','fields':'errrrm'}]
}
thingClientResource.post(things)
Tom Manterfield
  • 6,515
  • 6
  • 36
  • 52
akaphenom
  • 6,728
  • 10
  • 59
  • 109
  • 1
    +1 Thanks for the example. Note, I didn't have to override __init__ in my Serializer, just the create method in my view class – Fiver Apr 01 '14 at 20:27
  • I didn't think to try it without the init, I was working off of the previous example. I will definitely try your modification out, and update my answer pending that experiment. Thanks for the "heads up". – akaphenom Apr 02 '14 at 12:45
  • 3
    I think the key is the inclusion of `many=True` in the `get_serializer` call – Fiver Apr 02 '14 at 13:31
  • It's been over a year since I wrote my answer and I struggle to remember what I had for breakfast, so take this for what it's worth: I seem to remember the only reason I had to overwrite my init to add the many flag was because I didn't want to directly instantiate the serializer class for some reason (hopefully it was a good one, but right now it escapes me). So yeah, the passing of many=True is the key here. The overridden init can be dropped. – Tom Manterfield Sep 11 '14 at 18:03
  • This example is the only one that works with django 1.9 – Georg Zimmer Apr 04 '16 at 21:07
  • I was working on similar problem, but in my case, I have to create multiple objects but some data might throw some error, so for many=true how can we do multiple partial update , i.e let say 6/10 got inserted – ratnesh Jul 15 '21 at 11:05
14

I think the best approach to respect the proposed architecture of the framework will be to create a mixin like this:

class CreateListModelMixin(object):

    def create(self, request, *args, **kwargs):
        """
            Create a list of model instances if a list is provided or a
            single model instance otherwise.
        """
        data = request.data
        if isinstance(data, list):
            serializer = self.get_serializer(data=request.data, many=True)
        else:
            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)

Then you can override the CreateModelMixin of ModelViewSet like this:

class <MyModel>ViewSet(CreateListModelMixin, viewsets.ModelViewSet):
    ...
    ...

Now in the client you can work like this:

var things = [    
    {'loads':'foo','of':'bar','fields':'buzz'},
    {'loads':'fizz','of':'bazz','fields':'errrrm'}
]
thingClientResource.post(things)

or

var thing = {
    'loads':'foo','of':'bar','fields':'buzz'
}
    
thingClientResource.post(thing)

EDIT:

As Roger Collins suggests in his response is more clever to overwrite the get_serializer method than the 'create'.

Danial
  • 362
  • 4
  • 18
Daniel Albarral
  • 412
  • 5
  • 12
10

You can simply overwrite the get_serializer method in your APIView and pass many=True into get_serializer of the base view like so:

class SomeAPIView(CreateAPIView):
    queryset = SomeModel.objects.all()
    serializer_class = SomeSerializer

    def get_serializer(self, instance=None, data=None, many=False, partial=False):
        return super(SomeAPIView, self).get_serializer(instance=instance, data=data, many=True, partial=partial)
TehQuila
  • 628
  • 1
  • 8
  • 19
  • 1
    When you implement this method you might get and "AssertionError" When a serializer is passed a `data` keyword argument you must call `.is_valid()` before attempting to access the serialized `.data` representation. You should either call `.is_valid()` first, or access `.initial_data` instead. – Philip Mutua Mar 29 '18 at 12:52
  • Try the next: `from rest_framework.fields import empty def get_serializer(self, instance=None, data=empty, many=False, partial=False): return super(SomeAPIView, self).get_serializer(instance=instance, data=data, many=True, partial=partial)` – Guillermo Hernandez Jul 30 '19 at 18:57
  • Is it possible to override the perform_create or perform_update methods with this and add additional generated values to the individual objects? – Steven Matthews Jan 16 '22 at 22:36
7

I came up with simple example in post

Serializers.py

from rest_framework import serializers
from movie.models import Movie

class MovieSerializer(serializers.ModelSerializer):

    class Meta:
        model = Movie
        fields = [
            'popularity',
            'director',
            'genre',
            'imdb_score',
            'name',
        ]  

Views.py

from rest_framework.response import Response
from rest_framework import generics
from .serializers import MovieSerializer
from movie.models import Movie
from rest_framework import status
from rest_framework.permissions import IsAuthenticated

class MovieList(generics.ListCreateAPIView):
    queryset = Movie.objects.all().order_by('-id')[:10]
    serializer_class = MovieSerializer
    permission_classes = (IsAuthenticated,)

    def list(self, request):
        queryset = self.get_queryset()
        serializer = MovieSerializer(queryset, many=True)
        return Response(serializer.data)

    def post(self, request, format=None):
        data = request.data
        if isinstance(data, list):  # <- is the main logic
            serializer = self.get_serializer(data=request.data, many=True)
        else:
            serializer = self.get_serializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

These line are the actual logic of Multiple Instance -

data = request.data
if isinstance(data, list):  # <- is the main logic
      serializer = self.get_serializer(data=request.data, many=True)
else:
      serializer = self.get_serializer(data=request.data)

If you are confused with many=True, see this

When we send data it will be inside list somewhat like this -

[
    {
        "popularity": 84.0,
        "director": "Stanley Kubrick",
        "genre": [
            1,
            6,
            10
        ],
        "imdb_score": 8.4,
        "name": "2001 : A Space Odyssey"
    },
    {
        "popularity": 84.0,
        "director": "Stanley Kubrick",
        "genre": [
            1,
            6,
            10
        ],
        "imdb_score": 8.4,
        "name": "2001 : A Space Odyssey"
    }
]
Cipher
  • 2,060
  • 3
  • 30
  • 58
4

The Generic Views page in Django REST Framework's documentation states that the ListCreateAPIView generic view is "used for read-write endpoints to represent a collection of model instances".

That's where I would start looking (and I'm going to actually, since we'll need this functionality in our project soon as well).

Note also that the examples on the Generic Views page happen to use ListCreateAPIView.

akaihola
  • 26,309
  • 7
  • 59
  • 69
  • I saw that; however, in the tutorial there were no examples that showed how to allow for the creation/update of multiple items. Questions such as do I nest the resources within a json object, should it be flat, what happens if only a subset of items do not validate, etc aren't documented. For now, I did a somewhat inelegant workaround where I loop over a teachers json object and use a teacher serializer to validate and save. Please let me know if you find out a better solution. Thanks – Chaz Feb 14 '13 at 19:37
  • Yes it looks like to be individual Create and the List functions. I don't think the solution of updating / creating multiple records is in there. – akaphenom Mar 06 '14 at 21:41
  • 3 out of 4 links are now broken – e4c5 Sep 22 '16 at 03:19
4

Most straightforward method I've come across:

    def post(self, request, *args, **kwargs):
        serializer = ThatSerializer(data=request.data, many=isinstance(request.data, list))
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
popen
  • 53
  • 2