Django REST Framework中SerializerMethodField POST请求为何抛500错误?
我在模型中希望将testfield字段以列表形式返回,为此创建了SerializerMethodField:
class testserializer(serializers.ModelSerializer): testfield_as_list = serializers.SerializerMethodField() def get_testfield_as_list(self,obj): return [obj.testfield]
该字段在GET请求时正常工作,但执行POST请求时,数据虽能正确存入数据库(管理后台可查),接口却返回500错误,抛出异常:
in get_testfield_as_list return [obj.tesfield]
'collections.OrderedDict' object has no attribute 'testfield'
我原本以为SerializerMethodField是只读字段,POST时不应生效,为何会报错?
补充:我使用了upsert功能实现仅更新变更字段,相关代码如下:
class TestViewSet(viewsets.ModelViewSet): queryset = Test.objects.all() serializer_class = testserializer permission_classes = [IsAdminUserOrReadOnly] def create(self, request, *args, **kwargs): serializer = testserializer(data=request.data,many=True) serializer.is_valid(raise_exception=True) test_object_list = [Test(**x) for x in serializer.validated_data] Test.objects.bulk_create( test_object_list, update_conflicts=True, unique_fields=['unique_id'], update_fields=['category','description','testfield']) headers = self.get_success_headers(serializer.data) return Response( serializer.data, status=status.HTTP_201_CREATED, headers=headers ) class Test(models.Model): def __str__(self): return self.pk unique_id = models.CharField(max_length=100) category = models.CharField(max_length=400) description = models.CharField(max_length=400) testfield = models.CharField(max_length=400)
完整报错堆栈:
Internal Server Error: /api/v1/boards/ Traceback (most recent call last):
File "folder\django\core\handlers\exception.py", line 55, in inner
response = get_response(request)
File "folder\django\core\handlers\base.py", line 197, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "folder\django\views\decorators\csrf.py", line 56, in wrapper_view
return view_func(*args, **kwargs)
File "folder\rest_framework\viewsets.py", line 125, in view
return self.dispatch(request, *args, **kwargs)
File "folder\rest_framework\views.py", line 509, in dispatch
response = self.handle_exception(exc)
File "folder\rest_framework\views.py", line 469, in handle_exception
self.raise_uncaught_exception(exc)
File "folder\rest_framework\views.py", line 480, in raise_uncaught_exception
raise exc
File "folder\rest_framework\views.py", line 506, in dispatch
response = handler(request, *args, **kwargs)
File "....\api\views.py", line 33, in create
headers = self.get_success_headers(serializer.data)
File "folder\rest_framework\serializers.py", line 768, in data
ret = super().data
File "folder\rest_framework\serializers.py", line 255, in data
self._data = self.to_representation(self.validated_data)
File "folder\rest_framework\serializers.py", line 686, in to_representation
return [
File "folder\rest_framework\serializers.py", line 687, in
self.child.to_representation(item) for item in iterable
File "folder\rest_framework\serializers.py", line 522, in to_representation
ret[field.field_name] = field.to_representation(attribute)
File "folder\rest_framework\fields.py", line 1838, in to_representation
return method(value)
File "....\api\serializers.py", line 12, in get_testfield_as_list
return [obj.testfield]
AttributeError: 'collections.OrderedDict' object has no attribute 'testfield'
SerializerMethodField确实是只读字段,不会参与反序列化(即不会处理POST请求的输入数据),但问题出在返回响应时调用了serializer.data——此时DRF会执行序列化操作,把validated_data(OrderedDict类型)传入to_representation方法,而SerializerMethodField的get_testfield_as_list方法会尝试访问obj.testfield,但obj此时是validated_data里的OrderedDict,不是数据库返回的Test模型实例,自然没有testfield属性。
简单说:POST时你用序列化器处理了输入数据,但返回响应时直接用这个输入序列化器的data属性,导致序列化过程中触发了只读字段的处理逻辑,而此时传入的对象不是模型实例而是字典。
有三种可行的解决办法:
方法一:重新查询数据库获取实例,序列化真实模型对象
在bulk_create之后,根据unique_id重新查询数据库中的Test实例,再用序列化器处理这些实例:
def create(self, request, *args, **kwargs): serializer = testserializer(data=request.data,many=True) serializer.is_valid(raise_exception=True) test_object_list = [Test(**x) for x in serializer.validated_data] unique_ids = [obj.unique_id for obj in test_object_list] Test.objects.bulk_create( test_object_list, update_conflicts=True, unique_fields=['unique_id'], update_fields=['category','description','testfield']) # 重新查询数据库中的实例 created_or_updated_instances = Test.objects.filter(unique_id__in=unique_ids) response_serializer = testserializer(created_or_updated_instances, many=True) headers = self.get_success_headers(response_serializer.data) return Response( response_serializer.data, status=status.HTTP_201_CREATED, headers=headers )
方法二:修改SerializerMethodField的get方法,兼容字典和模型实例
在get_testfield_as_list方法中判断传入的obj类型,如果是字典就用键取值,如果是模型实例就用属性访问:
def get_testfield_as_list(self, obj): # 兼容模型实例和字典(validated_data) testfield_value = obj.testfield if hasattr(obj, 'testfield') else obj.get('testfield') return [testfield_value]
方法三:拆分输入输出序列化器
创建两个序列化器,输入序列化器只处理需要的字段,输出序列化器包含testfield_as_list字段:
# 输入序列化器,仅处理需要的字段 class TestInputSerializer(serializers.ModelSerializer): class Meta: model = Test fields = ['unique_id', 'category', 'description', 'testfield'] # 输出序列化器,包含只读的列表字段 class TestOutputSerializer(serializers.ModelSerializer): testfield_as_list = serializers.SerializerMethodField() def get_testfield_as_list(self, obj): return [obj.testfield] class Meta: model = Test fields = ['unique_id', 'category', 'description', 'testfield', 'testfield_as_list']
然后在ViewSet的create方法中使用输入序列化器处理请求数据,输出时用输出序列化器处理模型实例:
def create(self, request, *args, **kwargs): serializer = TestInputSerializer(data=request.data,many=True) serializer.is_valid(raise_exception=True) test_object_list = [Test(**x) for x in serializer.validated_data] unique_ids = [obj.unique_id for obj in test_object_list] Test.objects.bulk_create( test_object_list, update_conflicts=True, unique_fields=['unique_id'], update_fields=['category','description','testfield']) created_or_updated_instances = Test.objects.filter(unique_id__in=unique_ids) response_serializer = TestOutputSerializer(created_or_updated_instances, many=True) headers = self.get_success_headers(response_serializer.data) return Response( response_serializer.data, status=status.HTTP_201_CREATED, headers=headers )
内容的提问来源于stack exchange,提问作者Jason

