我有两个序列化器: PostSerializer 和 PostImageSerializer ,它们都继承了DRF ModelSerializer。 PostImage模型通过related_name ='photos'与Post链接。
由于我希望序列化程序执行 update ,因此PostSerializer会按照DRF官方文档中的说明覆盖ModelSerializer的update()方法。
class PostSerializer(serializers.ModelSerializer):
photos = PostImageSerializer(many=True)
class Meta:
model = Post
fields = ('title', 'content')
def update(self, instance, validated_data):
photos_data = validated_data.pop('photos')
for photo in photos_data:
PostImage.objects.create(post=instance, image=photo)
return super(PostSerializer, self).update(instance, validated_data)
class PostImageSerializer(serializer.ModelSerializer):
class Meta:
model = PostImage
fields = ('image', 'post')
我还定义了一个继承ModelViewSet的 ViewSet 。
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
最后,将PostViewSet注册到DefaultRouter。 (省略的代码)
目标很简单。
我得到400错误响应,如下所示。
{
“相片”: [ “这是必填栏。” ],
“标题”:[ “这是必填栏。” ],
“内容”:[ “这是必填栏。” ]
}
(请注意错误消息可能与DRF错误消息不完全匹配,因为它们已被翻译。)
很明显,没有应用我的PUT字段。 因此,我一直在研究Django rest框架源代码本身,并在ViewSet update() method continues to fail中发现了序列化程序验证。
我怀疑,因为我不是通过JSON而是通过使用键值对的表单数据来请求,所以request.data没有得到正确验证。
但是,我应该在请求中包含多个图像,这意味着纯JSON无法正常工作。
这种情况下最清晰的解决方案是什么?
谢谢。
正如Neil指出的那样,我在PostSerializer的update()方法的第一行添加了print(self)。但是,我的控制台上未打印任何内容。
我认为这是由于我的上述问题所致,因为调用序列化程序update()方法的perform_update()方法称为 AFTER serializer is validated。
因此,我的问题的主要概念可以缩小到以下范围。
再次感谢。
答案 0 :(得分:2)
首先,您需要设置标题:
Content-Type: multipart/form-data;
但是,如果您在邮递员中设置表单数据,则此标头应为 默认。
您不能将图像作为json数据发送(除非您将其编码为字符串并在服务器端解码为图像,例如base64)。
默认情况下,在DRF PUT 中需要所有字段。如果您只想设置部分字段,则需要使用 PATCH 。
要解决此问题并使用 PUT 更新部分字段,您有两个选择:
您可以覆盖viewset update 方法以始终更新部分序列化程序(仅更改提供的字段):
def update(self, request, *args, **kwargs):
partial = True # Here I change partial to True
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response(serializer.data)
添加
rest_framework.parsers.MultiPartParser
将主要设置文件添加到REST_FRAMEWORK字典:
REST_FRAMEWORK = {
...
'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.JSONParser',
'rest_framework.parsers.MultiPartParser',
)
}
在查看序列化程序时,很奇怪,您没有从 PostSerializer 中得到错误,因为您没有在Meta.fields元组中添加“照片”字段。
在这种情况下,我会提供更多建议:
答案 1 :(得分:1)
解决方案有些混合,或者是@Neil和@mon的答案。但是,我会理顺一下。
现在,邮递员提交的表单数据包含2个键值对(请参阅我在原始问题中上传的照片)。一个是与多个照片文件链接的“照片”关键字段,另一个是与“类似于JSON的字符串” 的一大块链接的“数据”关键字段。尽管这是一种与文件一起发布或放置数据的公平方法,但是DRF MultiPartParser或JSONParser无法正确解析这些文件。
我收到错误消息的原因很简单。 self.get_serializer(instance, data=request.data, partial=partial
中的ModelViewSet
方法(尤其是UpdateModelMixin)无法理解request.data
的一部分。
当前request.data
来自提交的表单数据如下。
<QueryDict: { "photos": [PhotoObject1, PhotoObject2, ... ],
"request": ["{'\n 'title': 'title test', \n 'content': 'content test'}",]
}>
仔细观察“请求”部分。该值是一个普通的string
对象。
但是我的PostSerializer期望request.data
看起来像下面的样子。
{ "photos": [{"image": ImageObject1, "post":1}, {"image": ImageObject2, "post":2}, ... ],
"title": "test title",
"content": "test content"
}
因此,让我们做一些实验,并根据上述JSON格式放入一些数据。 即
{ "photos": [{"image": "http://tny.im/gMU", "post": 1}],
"title" : "test title",
"content": "test content"
}
您将收到以下错误消息。
“照片”:[{“图像”:[“提交的数据不是文件。”]}]
这意味着每个数据都已正确提交,但是图像URL http://tny.im/gMU不是文件而是字符串。
现在,整个问题的原因已经很清楚了。解析器需要修复,以便序列化程序可以理解提交的表单数据。
1。编写新的解析器
新的解析器应将“类似于JSON”的字符串解析为正确的JSON数据。我从here.
借了MultipartJSONParser此解析器的作用很简单。如果我们使用键“ data”提交“类似于JSON”的字符串,请从rest_framework调用json
并进行解析。之后,返回带有请求文件的已解析JSON。
class MultipartJsonParser(parsers.MultiPartParser):
# https://stackoverflow.com/a/50514022/8897256
def parse(self, stream, media_type=None, parser_context=None):
result = super().parse(
stream,
media_type=media_type,
parser_context=parser_context
)
data = {}
data = json.loads(result.data["data"])
qdict = QueryDict('', mutable=True)
qdict.update(data)
return parsers.DataAndFiles(qdict, result.files)
2。重新设计序列化器
官方DRF文档建议使用嵌套的序列化程序来更新或创建相关对象。但是,我们有一个重大缺陷,即InMemoryFileObject无法转换为序列化程序期望的正确格式。为此,我们应该
update
方法request.data
中弹出“照片”键值对request.data
。这是因为我们的PostSerializer期望键名是“照片”。但是,基本上request.data
是默认情况下不可变的QuerySet。我非常怀疑是否必须强制更改QuerySet。因此,我宁愿将PostImage创建过程委托给update()
的{{1}}方法。在这种情况下,我们不再需要定义ModelViewSet
。
只需执行以下操作:
nested serializer
3。从class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = '__all__'
class PostImageSerializer(serializer.ModelSerializer):
class Meta:
model = PostImage
fields = '__all__'
覆盖update()
方法
为了利用我们的Parser类,我们需要显式指定它。我们将合并PATCH和PUT行为,因此设置ModelViewSet
。正如我们前面所看到的,图像文件带有键“照片”,因此弹出值并创建每个“照片”实例。
最后,由于我们新设计了解析器,将简单的“类似于JSON”字符串转换为常规JSON数据。因此,只需简单地将partial=True
和serializer_class
中的内容放进去。
perform_update
该解决方案有效,但是我不确定这是保存与外键相关的模型的最干净的方法。我强烈感觉是应该保存相关模型的是串行器。就像文档中指出的那样,文件以外的数据都以这种方式保存。如果有人可以告诉我更微妙的方法,那么我将不胜感激。