使用JSONField()获取非字段错误

5 投票
1 回答
1415 浏览
提问于 2025-04-18 18:04

我正在尝试使用 Django Rest Framework 发起一个 PATCH 请求,但遇到了以下错误:

{"image_data": [{"non_field_errors": ["Invalid data"]}]

我知道 JSONField() 可能会出现一些问题,所以我通过添加 to_nativefrom_native 来处理这个问题。但是,我仍然遇到这个问题。我觉得 JSONField() 根本不是问题,但提到一下也没坏处。

我认为我在更新相关字段的方式上做错了什么根本性的事情。

下面是代码...

模型:

class Photo(models.Model):
    user = models.ForeignKey(AppUser, help_text="Item belongs to.")
    image_data = models.ForeignKey("PhotoData", null=True, blank=True)


class PhotoData(models.Model):
    thisdata = JSONField()

序列化器:

class ExternalJSONField(serializers.WritableField):
    def to_native(self, obj):
        return json.dumps(obj)

    def from_native(self, value):
        try:
            val = json.loads(value)
        except TypeError:
            raise serializers.ValidationError(
                "Could not load json <{}>".format(value)
            )
        return val

class PhotoDataSerializer(serializers.ModelSerializer):

    thisdata = ExternalJSONField()
    class Meta:
        model = PhotoData
        fields = ("id", "thisdata")


class PhotoSerializer(serializers.ModelSerializer):

    image_data = PhotoDataSerializer()

    class Meta:
        model = Photo
        fields = ("id","user", "image_data")

PATCH 请求:

> payload = {"image_data": {"thisdata": "{}"}}
> requests.patch("/photo/123/",payload )

我也尝试过:

> payload = {"image_data": [{"thisdata": "{}"}]}
> requests.patch("/photo/123/",payload )

但还是出现了同样的错误:

[{"non_field_errors": ["无效数据"]}]

1 个回答

5

Django Rest Framework(DRF)中的关系序列化的最初想法是保持相关字段的值不变。这意味着你的数据包应该包含一个PhotoData对象的pk(主键),而不是它的数据集。就像在模型中,你不能把一个字典直接赋值给外键字段。

好的(仅适用于有问题的serializers.PrimaryKeyRelatedField):

   payload = {"image_data": 2}

坏的(在DRF中默认不工作):

   payload = {"image_data": {'thisdata': '{}'}}

实际上,你提供的数据模型根本不需要PhotoData(你可以把thisdata字段移动到Photo中),但我们假设你有一个特殊情况,即使Python的“禅”也说过特殊情况不足以打破规则。

所以,这里有一些可能的方法:

使用字段序列化器(你原来的方法)

你现在想做的事情是可能的,但解决方案非常丑陋。你可以创建一个PhotoDataField对我有效,但不是可以直接使用的代码,仅用于演示

class PhotoDataField(serializers.PrimaryKeyRelatedField):

    def field_to_native(self, *args):
        """
        Use field_to_native from RelatedField for correct `to_native` result
        """
        return super(serializers.RelatedField, self).field_to_native(*args)

    # Prepare value to output
    def to_native(self, obj):
        if isinstance(obj, PhotoData):
            return obj.thisdata
        return super(PhotoDataField, self).to_native(obj)

    # Handle input value
    def field_from_native(self, data, files, field_name, into):
        try:
            int(data['image_data'])
        except ValueError:
            # Looks like we have a data for `thisdata` field here.
            # So let's do write this to PhotoData model right now.
            # Why? Because you can't do anything with `image_data` in further.
            if not self.root.object.image_data:
                # Create a new `PhotoData` instance and use it.
                self.root.object.image_data = PhotoData.objects.create()
            self.root.object.image_data.thisdata = data['image_data']
            self.root.object.image_data.save()

            return data['image_data']
        except KeyError:
            pass
        # So native behaviour works (e.g. via web GUI)
        return super(PhotoDataField, self).field_from_native(data, files, field_name, into)

并在PhotoSerializer中使用它

class PhotoSerializer(serializers.ModelSerializer):

    image_data = PhotoDataField(read_only=False, source='image_data')

    class Meta:
        model = Photo
        fields = ("id", "user", "image_data")

这样请求就能正常工作

payload = {"image_data": '{}'}
resp = requests.patch(request.build_absolute_uri("/api/photo/1/"), payload)

而且“好的”请求也可以

photodata = PhotoData.objects.get(pk=1)
payload = {"image_data": photodata.pk}
resp = requests.patch(request.build_absolute_uri("/api/photo/1/"), payload)

最终你会在GET请求中看到"image_data": <photodata的thisdata值>,

但是,即使你用这种方法解决了验证问题,仍然会非常麻烦,正如我代码中所示(这是DRF在你想“打破正常工作流程”时能提供的唯一解决方案,Tastypie提供了更多选择)。

规范化你的代码并使用@action(推荐)

class PhotoDataSerializer(serializers.ModelSerializer):
    class Meta:
        model = PhotoData
        fields = ("id", "thisdata")


class PhotoSerializer(serializers.ModelSerializer):
    image_data = PhotoDataSerializer()  # or serializers.RelatedField

    class Meta:
        model = Photo
        fields = ("id", "user", "image_data", "test")

现在在你的API视图中定义一个特定的方法,这样你就可以用它来设置任何照片的数据

from rest_framework import viewsets, routers, generics
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status

# ViewSets define the view behavior.


class PhotoViewSet(viewsets.ModelViewSet):
    model = Photo
    serializer_class = PhotoSerializer

    @action(methods=['PATCH'])
    def set_photodata(self, request, pk=None):
        photo = self.get_object()
        serializer = PhotoDataSerializer(data=request.DATA)
        if serializer.is_valid():
            if not photo.image_data:
                photo.image_data = PhotoData.objects.create()
                photo.save()
            photo.image_data.thisdata = serializer.data
            photo.image_data.save()
            return Response({'status': 'ok'})
        else:
            return Response(serializer.errors,
                            status=status.HTTP_400_BAD_REQUEST)

现在你可以做几乎和现在一样的请求,但你在代码中有了更多的扩展性和责任划分。查看URL,当你有@action修饰的方法时,它会被附加上去。

payload = {"thisdata": '{"test": "ok"}'}
resp = requests.patch(request.build_absolute_uri("/api/photo/1/set_photodata/"), payload)

希望这对你有帮助。

撰写回答