Tuesday, August 15, 2017

How to access serializer.data on ListSerializer parent class in DRF?

Leave a Comment

I'm getting an error when trying to access serializer.data before returning it in the Response(serializer.data, status=something):

Getting KeyError when attempting to get a value for field <field> on serializer <serializer>.

This occurs on all fields (because it turns out I'm trying to access .data on the parent and not the child, see below)

The class definition looks like this:

class BulkProductSerializer(serializers.ModelSerializer):      list_serializer_class = CustomProductListSerializer      user = serializers.CharField(source='fk_user.username', read_only=False)      class Meta:         model = Product         fields = (             'user',             'uuid',             'product_code',             ...,         ) 

CustomProductListSerializer is a serializers.ListSerializer and has an overridden save() method that allows it to correctly handle bulk create and update.

Here's an example view from the bulk Product ViewSet:

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)     serializer.save()     pdb.set_trace()     return Response(serializer.data, status=status.HTTP_200_OK) 

Trying to access serializer.data at the trace (or the line after, obviously) causes the error. Here's the full trace (tl;dr skip below where I diagnose with debugger):

 Traceback (most recent call last):   File "/lib/python3.5/site-packages/django/core/handlers/exception.py", line 41, in inner     response = get_response(request)   File "/lib/python3.5/site-packages/django/core/handlers/base.py", line 249, in _legacy_get_response     response = self._get_response(request)   File "/lib/python3.5/site-packages/django/core/handlers/base.py", line 187, in _get_response     response = self.process_exception_by_middleware(e, request)   File "/lib/python3.5/site-packages/django/core/handlers/base.py", line 185, in _get_response     response = wrapped_callback(request, *callback_args, **callback_kwargs)   File "/lib/python3.5/site-packages/django/views/decorators/csrf.py", line 58, in wrapped_view     return view_func(*args, **kwargs)   File "/lib/python3.5/site-packages/rest_framework/viewsets.py", line 86, in view     return self.dispatch(request, *args, **kwargs)   File "/lib/python3.5/site-packages/rest_framework/views.py", line 489, in dispatch     response = self.handle_exception(exc)   File "/lib/python3.5/site-packages/rest_framework/views.py", line 449, in handle_exception     self.raise_uncaught_exception(exc)   File "/lib/python3.5/site-packages/rest_framework/views.py", line 486, in dispatch     response = handler(request, *args, **kwargs)   File "/application/siop/views/API/product.py", line 184, in partial_update     return Response(serializer.data, status=status.HTTP_200_OK)   File "/lib/python3.5/site-packages/rest_framework/serializers.py", line 739, in data     ret = super(ListSerializer, self).data   File "/lib/python3.5/site-packages/rest_framework/serializers.py", line 265, in data     self._data = self.to_representation(self.validated_data)   File "/lib/python3.5/site-packages/rest_framework/serializers.py", line 657, in to_representation     self.child.to_representation(item) for item in iterable   File "/lib/python3.5/site-packages/rest_framework/serializers.py", line 657, in <listcomp>     self.child.to_representation(item) for item in iterable   File "/lib/python3.5/site-packages/rest_framework/serializers.py", line 488, in to_representation     attribute = field.get_attribute(instance)   File "/lib/python3.5/site-packages/rest_framework/fields.py", line 464, in get_attribute     raise type(exc)(msg) KeyError: "Got KeyError when attempting to get a value for field `user` on serializer `BulkProductSerializer`.\nThe serializer field might be named incorrectly and not match any attribute or key on the `OrderedDict` instance.\nOriginal exception text was: 'fk_user'." 

At the L657 of the traceback (source here) I've got:

iterable = data.all() if isinstance(data, models.Manager) else data return [     self.child.to_representation(item) for item in iterable ] 

This made me wonder (digging further down in the trace) why the serializer.fields were not available. I suspected it was because the serializer was a CustomProductListSerializer parent, and not a BulkProductSerializer child, and I was right. In the pdb trace just before returning the Response(serializer.data):

(Pdb) serializer.fields *** AttributeError: 'CustomProductListSerializer' object has no attribute 'fields' (Pdb) serializer.child.fields {'uuid': UUIDField(read_only=False, required=False, validators=[]) ...(etc)} (Pdb) 'user' in serializer.child.fields True (Pdb) serializer.data *** KeyError: "Got KeyError when attempting to get a value for field `user` on serializer `BulkProductSerializer`.\nThe serializer field might be named incorrectly and not match any attribute or key on the `OrderedDict` instance.\nOriginal exception text was: 'fk_user'." (Pdb) serializer.child.data {'uuid': '08ec13c0-ab6c-45d4-89ab-400019874c63', ...(etc)} 

OK, so what's the right way to get the complete serializer.data and return it in the resopnse for the parent serializer class in the situation described by partial_update in my ViewSet?

Edit:

class CustomProductListSerializer(serializers.ListSerializer):      def save(self):         instances = []         result = []         pdb.set_trace()         for obj in self.validated_data:             uuid = obj.get('uuid', None)             if uuid:                 instance = get_object_or_404(Product, uuid=uuid)                 # Specify which fields to update, otherwise save() tries to SQL SET all fields.                 # Gotcha: remove the primary key, because update_fields will throw exception.                 # see https://stackoverflow.com/a/45494046                 update_fields = [k for k,v in obj.items() if k != 'uuid']                 for k, v in obj.items():                     if k != 'uuid':                         setattr(instance, k, v)                 instance.save(update_fields=update_fields)                 result.append(instance)             else:                 instances.append(Product(**obj))          if len(instances) > 0:             Product.objects.bulk_create(instances)             result += instances          return result 

4 Answers

Answers 1

As mentioned in the comment i still think the exception could be because of the user field in BulkProductSerializer class, not really anything to do with ListSerializer

There might be another minor error (but important) in the serializer DRF as mentioned in the documentation here. Here is how to specify a list_serializer_class:

class CustomListSerializer(serializers.ListSerializer):     ...  class CustomSerializer(serializers.Serializer):     ...     class Meta:         list_serializer_class = CustomListSerializer 

Note that it's specified inside of the Meta class, not outside. So i think in your code, it will not understand to switch to the List Serializer with many=True. That should cause the not-updating problem.

Answers 2

At the point in the trace where I try to access serializer.data and get the KeyError, I note that serializer.data only contains key/vaule pairs from the initial_data, not the instance data (hence, I suppose, the KeyError; some model fields' keys are not present as it is a partial_update request). However, serializer.child.data does contain all the instance data for the last child in the list.

So, I go to the rest_framework/serializers.py source where data is defined:

249    @property 250    def data(self): 251        if hasattr(self, 'initial_data') and not hasattr(self, '_validated_data'): 252            msg = ( 253                'When a serializer is passed a `data` keyword argument you ' 254                'must call `.is_valid()` before attempting to access the ' 255                'serialized `.data` representation.\n' 256                'You should either call `.is_valid()` first, ' 257                'or access `.initial_data` instead.' 258            ) 259            raise AssertionError(msg) 260 261        if not hasattr(self, '_data'): 262            if self.instance is not None and not getattr(self, '_errors', None): 263                self._data = self.to_representation(self.instance) 264            elif hasattr(self, '_validated_data') and not getattr(self, '_errors', None): 265                self._data = self.to_representation(self.validated_data) 266            else: 267                self._data = self.get_initial() 268        return self._data 

Line 265 is problematic. I can replicate the error by calling serializer.child.to_representation({'uuid': '87956604-fbcb-4244-bda3-9e39075d510a', 'product_code': 'foobar'}) at the breakpoint.

Calling partial_update() works fine on a single instance (because self.instance is set, self.to_representation(self.instance) works). However, for a bulk partial_update() implementation, self.validated_data is missing model fields, and to_representation() won't work, so I won't be able to access the .data property.

One option would be to maintain some sort of self.instances list of Product instances, and override the definition of data on line 265:

self._data = self.to_representation(self.instances) 

I'd really prefer an answer from someone more experienced in this sort of problem though, as I'm not sure if that's a sensible solution, hence I'm leaving the bounty open in the hope that someone can suggest something smarter to do.

Answers 3

Remove source if you are using Django auth model and set read_only=True.

user = serializers.CharField(read_only=True)

Hope this works for you

Answers 4

You have defined user field on BulkProductSerializer as writable but have not told the serializer how to handle it...

The easiest way to correct this is to use a SlugRelatedField:

class BulkProductSerializer(serializers.ModelSerializer):      list_serializer_class = CustomProductListSerializer      user = serializers.SlugRelatedField(                             slug_field='username',                             queryset=UserModel.objects.all(),                             source='fk_user'     )      class Meta:         model = Product         fields = (             'user',             'uuid',             'product_code',             ...,         ) 

This should handle nicely errors, for example when username does not exist...

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment