109

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!

1

9 Answers 9

99

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.

Sign up to request clarification or add additional context in comments.

9 Comments

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.
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 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.
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.
doesn't work for me { "non_field_errors": [ "Invalid data. Expected a dictionary, but got list." ] }
|
78

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)

5 Comments

This made my day! I confirm this works fine and accepts both lists and dict.
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).
kwargs.get('data', {}) will return a QueryDict, and thus fail the ininstance, so many will not be set to True.
@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 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.
62

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)

9 Comments

This answer seems more straight forward and how I would expect this functionality to be implemented.
This one works and the top voted answer doesn't work for me.
You can additionally add a transaction.atomic() block to make sure all elements are added
This deserves more vote as this was the only one that worked out for me and it's pretty straightforward too.
I ran into the: Expected a dictionary, but got list. error in the accepted answer and this one fixed it for me. Thanks.
|
17

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'.

1 Comment

Have a feeling Roger is a guy
15

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)

6 Comments

+1 Thanks for the example. Note, I didn't have to override init in my Serializer, just the create method in my view class
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".
I think the key is the inclusion of many=True in the get_serializer call
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.
This example is the only one that works with django 1.9
|
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)

3 Comments

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.
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)
Is it possible to override the perform_create or perform_update methods with this and add additional generated values to the individual objects?
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"
    }
]

Comments

5

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.

3 Comments

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
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.
3 out of 4 links are now broken
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)

1 Comment

You could simply set, many=True and it will still work

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.