DRF 序列化器跨多个嵌套字段验证输入

问题描述 投票:0回答:2

我正在使用 DRF 串行器来处理具有单位测量值的模型。这是我的模型:

class Cargo(models.Model):
    length = models.FloatField()
    height = models.FloatField()
    weight = models.FloatField()
    input_length_unit = models.IntegerField(choices=LengthUnits.choices)
    input_weight_unit = models.IntegerField(choices=WeightUnits.choices)

我有以下序列化器可以将

{"length": {"value": 10, "unit": 1}, ...}
等数据转换为我的模型架构:

class FloatWithUnitSerializer(serializers.Serializer):
    value = serializers.FloatField()
    unit = serializers.IntegerField()

    def __init__(self, unit_type, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.unit_type = unit_type

    def to_representation(self, instance):
        value = getattr(instance, self.field_name)
        unit = getattr(instance, f"input_{self.unit_type}_unit")
        # convert from database unit to input unit
        value = value * UNITS[self.unit_type][unit]

        return {"value": value, "unit": unit}

    def to_internal_value(self, data):
        # convert from input unit to database unit
        value = data["value"] / UNITS[self.unit_type][data["unit"]]

        return {self.field_name: value, f"input_{self.unit_type}_unit": data["unit"]}

class CargoSerializer(serializers.ModelSerializer):
    length = FloatWithUnitSerializer("length", required=False, source="*")
    height = FloatWithUnitSerializer("length", required=False, source="*")
    weight = FloatWithUnitSerializer("weight", required=False, source="*")

    class Meta:
        model = models.Cargo
        fields = ["length", "height", "weight", "input_length_unit", "input_weight_unit"]

这可行,但现在我想防止单位混合。例如,给定单位类型“长度”,其中包括“长度”和“高度”字段(单位为米、英尺等),他们需要为两者发布相同的单位,例如

{"length": {"value": 10, "unit": 1}, "height": {"value:15", "unit": 1}}
。如果它们传递具有不同单位的相同单位类型的 2 个字段,例如
{"length": {"value": 10, "unit": 1}, "height": {"value:15", "unit": 2}}
,我想提出一个ValidationError。我还将添加其他单位类型,例如面积和体积。我无法在 FloatWithUnitSerializer 内部验证这一点,因为我只有单个字段的数据,并且我不确定如何在 CargoSerializer 中验证这一点 - 当调用我的验证函数时, FloatWithUnitSerializer.to_internal_value 被调用,所以我只有长度、高度、input_length_unit等字段,并且不知道是否传递了多个长度单位。我如何验证这一点,或者是否有更简单的方法可以构建它?谢谢。

django django-rest-framework units-of-measurement
2个回答
0
投票

我最终像这样重写了 to_internal_value :

class CargoSerializer(serializers.ModelSerializer):
    length = FloatWithUnitSerializer("length", required=False, source="*")
    height = FloatWithUnitSerializer("length", required=False, source="*")
    weight = FloatWithUnitSerializer("weight", required=False, source="*")

    class Meta:
        model = models.Cargo
        fields = ["length", "height", "weight", "input_length_unit", "input_weight_unit"]

    def to_internal_value(self, data):
        # Note the only change from Django's default to_internal_value is called out below.
        if not isinstance(data, Mapping):
            message = self.error_messages["invalid"].format(datatype=type(data).__name__)
            raise ValidationError({api_settings.NON_FIELD_ERRORS_KEY: [message]}, code="invalid")

        ret = OrderedDict()
        errors = OrderedDict()
        fields = self._writable_fields

        for field in fields:
            validate_method = getattr(self, "validate_" + field.field_name, None)
            primitive_value = field.get_value(data)
            try:
                validated_value = field.run_validation(primitive_value)
                if validate_method is not None:
                    validated_value = validate_method(validated_value)
            except ValidationError as exc:
                errors[field.field_name] = exc.detail
            except DjangoValidationError as exc:
                errors[field.field_name] = get_error_detail(exc)
            except SkipField:
                pass
            else:
                # The only change: Don't allow multiple units per unit type
                if isinstance(field, FloatWithUnitSerializer):
                    unit_field_name, unit = next(
                        (k, v) for k, v in validated_value.items() if k.startswith("input_")
                    )

                    current_unit = ret.get(unit_field_name)

                    if current_unit is not None and current_unit != unit:
                        errors[api_settings.NON_FIELD_ERRORS_KEY] = "Received mixed units."
                set_value(ret, field.source_attrs, validated_value)

        if errors:
            raise ValidationError(errors)

        return ret

-1
投票

要实现此验证,您可以重写

validate
中的
CargoSerializer
方法。在该方法中,您可以访问所有已验证的数据,包括
length
height
等字段,然后检查是否存在同一单位类型的混合单位。如果您发现混合单位,请提出
serializers.ValidationError

以下是如何修改

CargoSerializer
的示例:

from rest_framework import serializers

class CargoSerializer(serializers.ModelSerializer):
    length = FloatWithUnitSerializer("length", required=False, source="*")
    height = FloatWithUnitSerializer("height", required=False, source="*")
    weight = FloatWithUnitSerializer("weight", required=False, source="*")

    class Meta:
        model = models.Cargo
        fields = ["length", "height", "weight", "input_length_unit", "input_weight_unit"]

    def validate(self, data):
        # Validate unit consistency within the same unit type
        length_unit = data.get("length", {}).get("unit")
        height_unit = data.get("height", {}).get("unit")
        weight_unit = data.get("weight", {}).get("unit")

        if length_unit is not None and height_unit is not None and length_unit != height_unit:
            raise serializers.ValidationError("Length and height must have the same unit.")

        # Add similar checks for other unit types (e.g., weight, area, volume)

        return data

在此示例中,

validate
方法检查长度和高度是否具有相同的单位。您可以根据需要将此模式扩展到其他单位类型。

这样,您就可以访问所有经过验证的数据,并且可以在将数据保存到模型之前执行跨字段验证。

© www.soinside.com 2019 - 2024. All rights reserved.