2

I'm trying to override my serializer's save() method (as per the docs) to support bulk instance creation. At the moment, I have something that looks like this (skip the code if you like, it's just for context. The real issue is I can't make any of my own serializer methods).

serializers.py

class BulkWidgetSerializer(serializers.ModelSerializer):
    """ Serialize the Widget data """

    #http://stackoverflow.com/questions/28200485/
    some_foreign_key = serializers.CharField(source='fk_fizzbuzz.name', read_only=False)

    class Meta:
        model = Widget
        fields = (
            'some_foreign_key',
            'uuid',
            'foobar',
        )
        # Normally we would set uuid to read_only, but then it won't be available in the self.validate()
        # method. We also need to take the validator off this field to remove the UNIQUE constraint, and
        # perform the validation ourselves.
        # See https://github.com/encode/django-rest-framework/issues/2996 and
        # https://stackoverflow.com/a/36334825/3790954
        extra_kwargs = {
            'uuid': {'read_only': False, 'validators': []},
        }

    def validate(self, data):
        return super(WidgetSerializer, self).validate(self.business_logic(data))

    def save(self):
        print("---------Calling save-----------")
        more_business_logic()
        instances = []
        for widget in self.validated_data:
            instances.append(Widget(**self.validated_data))
        Widget.objects.bulk_create(instances)
        return instances

viewset.py

class WidgetViewSet(viewsets.ModelViewSet):
    permission_classes = (permissions.IsAuthenticated,)
    serializer_class = BulkWidgetSerializer
    pagination_class = WidgetViewSetPagination
    lookup_field = 'uuid'

    def partial_update(self, request):
        serializer = self.get_serializer(data=request.data,
                    many=isinstance(request.data, list),
                    partial=True)
        if not serializer.is_valid():
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        pdb.set_trace()
        serializer.save()
        return Response(serializer.data, status=status.HTTP_200_OK)

Then I get new Wiget instances in the database, but their properties indicate that the more_business_logic() call in save() didn't occur. However, I do get feedback indicating that business_logic in validate() call occurred.

I presume from this that I'm somehow still stuck with the super class's save()? How can I override this method?

Edit:

When I rename save() to newsave() in both files, and try to call it in the ViewSet, I get:

AttributeError: 'ListSerializer' object has no attribute 'newsave'

What's going on? Inspecting with pdb at the breakpoint shows that it is indeed a BulkWidgetSerializer. Inspecting in the shell shows newsave is definitely a method of that class:

>>>'newsave' in [func for func in dir(BulkWidgetSerializer) if callable(getattr(BulkWidgetSerializer, func))]
True

Moreover, if I create my own test method in the serializer class:

def test_method(self):
    print("Successful test method")

I can't call that either!

>>> serializer.test_method()
AttributeError: 'ListSerializer' object has no attribute 'test_method'
Escher
  • 5,418
  • 12
  • 54
  • 101
  • For situations like these you should consider using `pdb`. Can you show us the code where you map your viewset with the child serializer? Also, what parent serializer have your override? – Raunak Agarwal Aug 02 '17 at 14:29
  • Yes, I can't invoke `pdb` in a function I can't call unfortunately. What do you mean parent/child serializer? There's only one level of serializer. – Escher Aug 02 '17 at 14:42
  • Your code sample is too restricted. For what we know you could call a different serializer than the one you think you call. – Linovia Aug 02 '17 at 16:29
  • I've added more code, and debugging output. The debugger shows that it is as described, and I'm still unable to implement a custom `save` method. – Escher Aug 02 '17 at 17:55

3 Answers3

6

Your BulkWidgetSerializer is wrapped by ListSerializer which is default behavior of DRF. That's why your new methods are missing.

If you instantiate any subclass of BaseSerializer with kwarg many=True the library wraps it with new ListSerializer with child set to your Serializer class.

Because of that you cannot override save() method to get desired effect. Try overriding many_init classmethod of your serializer to provide custom ListSerializer which implements desired behavior, as shown in DRF documentation.

Secondly it's better to override create() or update() methods instead of save() which calls one of them.

Your implementation could look something like that:

class CustomListSerializer(serializers.ListSerializer):
    def create(self, validated_data):
        more_business_logic()
        instances = [
            Widget(**attrs) for attrs in validated_data
        ]
        return Widget.objects.bulk_create(instances)

And then in BulkWidgetSerializer:

@classmethod
def many_init(cls, *args, **kwargs):
    kwargs['child'] = cls()
    return CustomListSerializer(*args, **kwargs)

A gotcha: don't forget to pass the right kwargs from the parent to the child, eg kwargs['child'] = cls( partial=kwargs.get('partial') ) if you rely on any of them in your child class during your overwritten methods to support bulk partial updates (eg validate()).

Escher
  • 5,418
  • 12
  • 54
  • 101
buoto
  • 405
  • 4
  • 13
5

It seems that you are instantiating your serializer with many=True. In this case the ListSerializer is instantiated internally (you can find the code for this in the class method rest_framework.serializers.BaseSerializer.many_init).

Hence the save() method of the ListSerializer is called. If you must override the save method, first create a custom list serializer:

class CustomListSerializer(serializers.ListSerializer):
    def save(self):
         ...

Then add this custom list serializer to your BulkWidgetSerializer by specifying list_serializer_class:

class Meta:
    list_serializer_class = CustomListSerializer

As specified by others, it is better to override either the create or update methods instead of save

arijeet
  • 1,858
  • 18
  • 26
2

You have been reading the wrong part of documentation and your approach is not correct.

The default implementation for multiple object creation is to simply call .create() for each item in the list. If you want to customize this behavior, you'll need to customize the .create() method on ListSerializer class that is used when many=True is passed.

This ensures your more_business_logic you want to happen will happen on each item you pass in that list.

As per the documentation - http://www.django-rest-framework.org/api-guide/serializers/#customizing-multiple-create

Borko Kovacev
  • 1,010
  • 2
  • 14
  • 33