Django REST framework post array of objects
Asked Answered
F

6

22

I am using Django REST framework for API and Angular SPA with Restangular to communicate with the API. Sometimes, I have to add more than one object using the API and I think I can send them together in an array and do this in one request.

I receive wrong input error when I'm trying to add more than one object from the REST framework web interface. I am passing objects or array of objects like below:

// this { "text": "gdhg", },{ "text": "gdhg", },{ "text": "gdhg", }
// or this [{ "text": "gdhg", },{ "text": "gdhg", },{ "text": "gdhg", }]

But I receive ParseError. Where I am wrong and what do I have to change to fix this?

Flue answered 5/4, 2014 at 12:45 Comment(0)
S
25

Another example that supports posting an array as well as posting a single object. Might be useful for anyone else looking for such an example.

class BookViewSet(mixins.CreateModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
    """
    ViewSet create and list books

    Usage single : POST
    {
        "name":"Killing Floor: A Jack Reacher Novel", 
        "author":"Lee Child"
    }

    Usage array : POST
    [{  
        "name":"Mr. Mercedes: A Novel (The Bill Hodges Trilogy)",
        "author":"Stephen King"
    },{
        "name":"Killing Floor: A Jack Reacher Novel", 
        "author":"Lee Child"
    }]
    """
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    search_fields = ('name','author')

    def create(self, request, *args, **kwargs):
        """
        #checks if post request data is an array initializes serializer with many=True
        else executes default CreateModelMixin.create function 
        """
        is_many = isinstance(request.data, list)
        if not is_many:
            return super(BookViewSet, self).create(request, *args, **kwargs)
        else:
            serializer = self.get_serializer(data=request.data, many=True)
            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)
Stolzer answered 11/10, 2015 at 8:45 Comment(1)
Excellent, I was worried I'd need to re-read the documentation to do something like this myself. Thanks to your answer, I didn't need to ;)Minervamines
F
6

I am not sure if the problem still exist. But the solution suggested by fiver did not work for me. What works for me is overriding the get_serializer method ONLY.

def get_serializer(self, instance=None, data=None,
                    files=None, many=True, partial=False):
    return super(ViewName, self).get_serializer(instance, data, files,
                                                    many, partial)

If you will notice I am setting default many=True in arguments of get_serializer. Apart from that nothing is required. Overridng of create method is also not required.

Also if you are defining the pre_save and post_save method in the views, expects the list(iterable) as the argument(as you are posting the list) of method not just a single object.

def post_save(self, objects, *args, **kwargs):
    """
    In the post_save, list of obj has been created
    """
    for obj in objects:
        do_something_with(obj)
Footpoundsecond answered 18/6, 2014 at 8:29 Comment(0)
U
4

Here's an example for setting up bulk POSTing in a ListCreateAPIView using the Django REST Framework:

class SomethingList(generics.ListCreateAPIView):
    model = Something
    serializer_class = SomethingSerializer

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.DATA, 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)

The important part here is the many=True argument to the get_serializer() method. Then, to make Angular play nice with this, you can define a service factory as:

.factory('Something', ['$resource', function ($resource) {
    return $resource(
        "url_to_something",
        {},
        {
            save: {
                method: 'POST',
                isArray: true
            }
        }
    );
}])

Where the important part is the isArray: true. If you want to preserve posting single JSON objects, you could change save above to something like saveBulk or similar.

Utilitarianism answered 5/4, 2014 at 16:3 Comment(1)
I did that with ModelViewSet and now receiving "'list' object has no attribute 'get'" because the request.DATA is list. How to fix this problem. I am testing it from the web interface of the api with sending json array of objects: [{ "text": "gdhg", },{ "text": "gdhg", },{ "text": "gdhg", }]Flue
C
1

Building on vibhor's answer:

class ListableViewMixin(object):
    def get_serializer(self, instance=None, data=None, many=False, *args, **kwargs):
        return super(ListableViewMixin, self).get_serializer(
            instance=instance, data=data, many=isinstance(instance, list) or isinstance(data, list),
            *args, **kwargs)

Make your view inherit from this mixin class to automatically determine if a many=True serializer should be used.

Ciel answered 5/11, 2015 at 22:9 Comment(0)
P
0

If you want to post a list you have to pass in JSON encoded data.

headers = {"Token": "35754sr7cvd7ryh454"}

recipients = [{'name': 'Ut est sed sed ipsa', 
               'email': '[email protected]', 
               'group': 'signers'},
              {'name': 'Development Ltda.', 
               'email': '[email protected]',
               'group': 'signers'}
             ]

requests.post(url, json=recipients, headers=headers)

requests.post(url, json=recipients, headers=headers)

Use json keyword argument (not data) so the data is encoded to JSON and the Content-Type header is set to application/json.

By default, Django Rest Framework assumes you are passing it a single object. To serialize a queryset or list of objects instead of a single object instance, you should pass the many=True flag when instantiating the serializer. You can then pass a queryset or list of objects to be serialized.

To do it, you'll have to override the .create() method of your view:

def create(self, request, *args, **kwargs):
    many = True if isinstance(request.data, list) else False

    serializer = self.get_serializer(data=request.data, many=many)
    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)

Documentation: https://www.django-rest-framework.org/api-guide/serializers/#dealing-with-multiple-objects

Protanopia answered 18/7, 2020 at 17:5 Comment(0)
C
0

I looked at all the solutions here, and most have one thing in common which is if the data is a list, then call get_serializer with many=True. Most of the solutions override the create method to do this, but I find it more concise to override get_serializer instead. Way less code, and it makes sense that the get_serializer should understand that list will always mean many=True. There is at least one other answer that touched on this but here is the python3 / 2024 version.

def get_serializer(self, *args, **kwargs):
    if isinstance(kwargs.get("data"), list):
        kwargs["many"] = True

    return super().get_serializer(*args, **kwargs)
Corrective answered 11/7 at 14:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.