Django REST中Multipart数据转字符串致接口验证失败问题
问题描述
向POST接口同时传递JSON数据和图片时,包含文件的request.data中sales字段被转为字符串,无法被识别为对象/字典,导致序列化验证失败,报错:
rest_framework.exceptions.ValidationError: {'sales': [ErrorDetail(string='Must specify at least one sales', code='blank')]}
测试数据
data = { "external": "90000001", "sales": [ { "total": 4, "quantities": {"xs": 2, "s": 4}, "product": { "type": self.type.id, "product_image": { "name": "First test files", "product_files": [ { # "file": base64.b64encode(f.read()).decode(), "file": test_file, "description": "Patterns file.", "type": "pattern_file", } ] }, }, } ], }
请求发送代码
res: Response = self.client.post(self.create_ext_so_url, data)
异常现象
在run_validation(...)中提取的sales数据为字符串类型:
>>> attrs.get("sales", []) "{'total': 4, 'quantities': {'xs': 2, 's': 4}, 'product': {'type': 1, 'product_image': {'name': 'First test files', 'product_files': [{'file': <SimpleUploadedFile: test.svg (image/svg+xml)>, 'description': 'Patterns file.', 'type': 'pattern_file'}]}}}"
相关代码
视图代码
@action(detail=False, methods=["POST"], url_path="create-sales") def create_sales_order(self, request: Request)-> Response: ser_class = self.get_serializer_class() ser = ser_class(data=request.data) ser.is_valid(raise_exception=True) ser.save() return Response(ser.data, status=HTTP_201_CREATED)
序列化器代码
class OrderSerializer(ModelSerializer[Order]): order_lines = LineSerializer(many=True) class Meta: model = Order fields = "__all__" def validate(self, attrs: Any) -> Any: order_lines = [ item for item in attrs.get("order_lines", []) if not is_empty_object(item) ] if ( not order_lines or not isinstance(order_lines, list) or len(order_lines) == 0 ): raise ValidationError( {"order_lines": ["Must specify at least one order line"]}, code="blank", ) for line in order_lines: if not line.get("product", None): raise ValidationError( { "order_lines": [ "Cannot create Order Line without a Porduct on it." ] }, code="blank", ) elif not line.get("product").get("style", None): raise ValidationError( {"order_lines": ["Poroduct must contain style information."]}, code="blank", ) return super(OrderSerializer, self).validate(attrs) def create(self, validated_data: Dict[str, Any]) -> Order: order_lines = validated_data.pop("order_lines") order: Order = super(OrderSerializer, self).create(validated_data) for item in order_lines: item["order"] = order if not item.get("product").get("id", None): product = Product.objects.create(**item.get("product")) item["product"] = product OrderLine.objects.create(**item) return order class LineSerializer(ModelSerializer[Line]): product = CreateProductSerializer() rg_id = CharField(required=False) class Meta: model = Line fields = "__all__" extra_kwargs = { "order": {"required": False}, } def create(self, validated_data: Dict[str, Any]) -> Line: p_ser = CreateProductSerializer(data=validated_data.pop("product")) p_ser.is_valid(raise_exception=True) validated_data["product"] = p_ser.save() return super(LineSerializer, self).create(validated_data) class CreateProductSerializer(ModelSerializer[Product]): style = CreateStyleSerializer(required=False) # type: ignore class Meta: model = Product fields = "__all__" def run_validation(self, data: Any = ...) -> Any | None: if not data: return super(CreateProductSerializer, self).run_validation(data) style = data.get("style", None) if not style: raise ValidationError( {"style": ["Product must specify a style."]}, code="blank", ) return super(CreateProductSerializer, self).run_validation(data) def create(self, validated_data: Dict[str, Any]) -> Product: style = validated_data.pop("style") s_ser = CreateStyleSerializer(data=style) s_ser.is_valid(raise_exception=True) validated_data["style"] = s_ser.save() return super(CreateProductSerializer, self).create(validated_data) class CreateStyleSerializer(ModelSerializer[Style]): style_files = StyleFileSerializer(many=True, required=False) class Meta: model = Style fields = "__all__" def run_validation(self, data: Any = ...) -> Any | None: svg = data.get("svg", None) style_files = data.get("style_files", None) if style_files and len(style_files) == 0: style_files = None if not svg and not style_files: raise ValidationError( { "svg": [ "Please provide either an svg or the style_files required to generate a `Style`" ] }, code="blank", ) return super(CreateStyleSerializer, self).run_validation(data) def create(self, validated_data: Dict[str, Any]) -> Style: style_files = validated_data.pop("style_files") style = super(CreateStyleSerializer, self).create(validated_data) style_files = [{**item, "style": style} for item in style_files] sf_ser = StyleFileSerializer(data=style_files, many=True) sf_ser.is_valid(raise_exception=True) sf_ser.save() return style
解决方案
核心问题是:当请求包含文件时,Django测试客户端默认将请求编码为multipart/form-data格式,嵌套JSON结构会被自动转为字符串,导致序列化器无法正确解析。同时测试数据字段名与序列化器定义不匹配,也会触发验证报错。
具体修复步骤:
修正测试数据字段名
从序列化器代码可见,OrderSerializer中定义的字段是order_lines,而非测试数据中的sales,需修改测试数据:data = { "external": "90000001", "order_lines": [ # 替换原有的sales为order_lines { "total": 4, "quantities": {"xs": 2, "s": 4}, "product": { "type": self.type.id, "product_image": { "name": "First test files", "product_files": [ { "file": test_file, "description": "Patterns file.", "type": "pattern_file", } ] }, }, } ], }指定请求格式为multipart
发送请求时明确指定format='multipart',确保测试客户端正确编码包含文件的请求:res: Response = self.client.post( self.create_ext_so_url, data, format='multipart' )确保文件字段序列化器配置正确
检查product_files对应的序列化器是否正确配置FileField,示例如下:class ProductFileSerializer(ModelSerializer): file = FileField() class Meta: model = ProductFile fields = ["file", "description", "type"]
验证效果
修改后重新运行测试,request.data中的order_lines应识别为列表对象,序列化器可正常解析数据并通过验证。
内容的提问来源于stack exchange,提问作者Sardar Faisal
相关产品推荐
相关产品推荐

