You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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结构会被自动转为字符串,导致序列化器无法正确解析。同时测试数据字段名与序列化器定义不匹配,也会触发验证报错。

具体修复步骤:

  1. 修正测试数据字段名
    从序列化器代码可见,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",
                            }
                        ]
                    },
                },
            }
        ],
    }
    
  2. 指定请求格式为multipart
    发送请求时明确指定format='multipart',确保测试客户端正确编码包含文件的请求:

    res: Response = self.client.post(
        self.create_ext_so_url,
        data,
        format='multipart'
    )
    
  3. 确保文件字段序列化器配置正确
    检查product_files对应的序列化器是否正确配置FileField,示例如下:

    class ProductFileSerializer(ModelSerializer):
        file = FileField()
    
        class Meta:
            model = ProductFile
            fields = ["file", "description", "type"]
    

验证效果

修改后重新运行测试,request.data中的order_lines应识别为列表对象,序列化器可正常解析数据并通过验证。


内容的提问来源于stack exchange,提问作者Sardar Faisal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 09:05:27