首页 > 解决方案 > Django Rest Framework (DRF) 在 to_internal_value 函数中将 JSON 字段设置为 Empty

问题描述

所以我正在尝试按照这个解决方案上传文件并将 JSON 数据发布到我的 API。我已经创建了解析器,将它放在我的视图集中。我什至在to_internal_value函数中接收到图像和 JSON 数据,我认为它在解析器解析数据之后运行。

但是,一旦to_internal_value运行该函数,该location字段就会设置为空。我重写了该函数以查看发生了什么,并且该field.get_value函数似乎返回了空值。

这是解析器:

class MultipartJsonParser(parsers.MultiPartParser):

    def parse(self, stream, media_type=None, parser_context=None):
        result = super().parse(
            stream,
            media_type=media_type,
            parser_context=parser_context
        )


        data = {}
       # for case1 with nested serializers
        # parse each field with json
        for key, value in result.data.items():
            if type(value) != str:
                data[key] = value
                continue
            if '{' in value or "[" in value:
                try:
                    data[key] = json.loads(value)
                except ValueError:
                    data[key] = value
            else:
                data[key] = value

        qdict = QueryDict('', mutable=True)
        print(data)
        qdict.update(data)
        return parsers.DataAndFiles(qdict, result.files)

这是序列化器:

class AssetSerializer(serializers.ModelSerializer):
    """
    Serializes data recieved/retrieved to/from endpoints concerning Assets.
    """
    asset_location = LocationSerializer(required=False,allow_null=True)
    asset_devices = serializers.PrimaryKeyRelatedField(many=True,queryset=Device.objects.all(),required=False)
    parent = serializers.PrimaryKeyRelatedField(queryset=Asset.objects.all(),required=False)
    busy_ranges = serializers.CharField(required=False)
    time = serializers.ReadOnlyField(
        source='datetime')
    status = serializers.CharField(source="current_status",required=False)

    class Meta:
        model = Asset
        fields = ['id','asset_devices','name','slug','buying_date','asset_location','weight','icon','enabled','parent','is_parent','time','status','busy_ranges']
        read_only_fields = ('id','slug','is_parent','busy_ranges')
        extra_kwargs = {
            'status': {
                'help_text': 'Status of the asset. Available options are "AV" for available, "LE" for Lent, "MT" for In Maintenance'
            }
        }
        #exclude_when_nested = {'device'}  # not an official DRF meta attribute ...
    

    def create(self,validated_data):
        location_data = validated_data.pop('asset_location')
        location = Location.objects.create(**location_data)
        validated_data['asset_location']=location
        owner = self.context['request'].user
        asset = Asset.objects.create(**validated_data,owner=owner)

        return asset



    def to_internal_value(self, data):
        """
        Dict of native values <- Dict of primitive datatypes.
        """
        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

        #The line below print(data) prints the following
        #<QueryDict: {'name': ['test asset'], 'weight': ['20'], 'asset_location': [{'x': '10', 'y': '30'}], 'icon': [<InMemoryUploadedFile: issues_mzali.png (image/png)>]}>

        print(data)
        

        for field in fields:
            validate_method = getattr(self, 'validate_' + field.field_name, None)
            primitive_value = field.get_value(data)
            print(primitive_value) #this line prints 'asset_location' field as <class 'rest_framework.fields.empty'> which gives KeyError in the create method
            #All other fields are printed fine.
            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:
                set_value(ret, field.source_attrs, validated_value)

        if errors:
            raise ValidationError(errors)

        return ret
    
    def update(self,instance,validated_data):
        try:
            location_data = validated_data['asset_location']

            if instance.asset_location is None:
                location = Location.objects.create(**location_data)
                instance.asset_location = location
                instance.save()

            else:
                instance_location = instance.asset_location
                for key,value in location_data.items():
                    setattr(instance_location,key,value)
                instance_location.save()
                instance.save()
            
            return instance


        except KeyError as e:
            print(e.message)
            return super(AssetSerializer,self).update(instance,validated_data)

请检查to_internal_value功能中的注释,您将更好地了解问题。

标签: djangodjango-rest-frameworkdjango-serializerdjango-rest-viewsets

解决方案


因此,在深入研究代码之后,field.get_value()嵌套序列化程序实际上从 rest_framework.html 调用 html.parse_html_dict(),它只验证带有点符号的字段。

简而言之,我需要以这种方式发送字段: asset_location.x asset_location.y

所以我只是发送了 JSON,并更改了我的解析器以将 JSON 字段更改为上述格式,这就是我的解析器现在的样子:

class MultipartJsonParser(parsers.MultiPartParser):

def parse(self, stream, media_type=None, parser_context=None):
    result = super().parse(
        stream,
        media_type=media_type,
        parser_context=parser_context
    )


    data = {}
   

    #For nested serializers, drf accepts values in dotted notaion. E.g if location is nested serializer.
    # It will accept location.x and location.y if data is to be entered in form fields.
    # the 2 nested for loops, ensures that the JSON data sent in form field is converted to the above format.
    #e.g if the key is asset_location. and it has x and y keys inside. It will be converted to asset_location.x, and asset_location.y


    for key, value in result.data.items():
        if type(value) != str:
            data[key] = value
            continue
        if '{' in value or "[" in value:
            try:
                data[key] = json.loads(value)
                if type(data[key]) == dict:
                    for inner_key,inner_value in data[key].items():
                        data[f'{key}.{inner_key}']=inner_value
            except ValueError:
                data[key] = value
        else:
            data[key] = value

    qdict = QueryDict('', mutable=True)
    qdict.update(data)
    return parsers.DataAndFiles(qdict, result.files)

推荐阅读