Django Rest Framework (DRF) sets JSON field to Empty in the to_internal_value function
Asked Answered
S

1

0

So I am trying to upload file AND Post JSON data to my API following this solution. I have created the Parser, placed it in my viewset. I even receive the image and the JSON data in the to_internal_value function, which i think runs AFTER the parser parses the data.

How ever, as soon as the to_internal_value function is run, the location field is set to empty. I overrided the function to see whats happening, and it seems the field.get_value function is returning the empty value.

Here's the parser:

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)

Here's the Serializer:

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)

Please check the comments in the to_internal_value function, you will get a better idea of the problem.

Steamship answered 28/7, 2021 at 6:40 Comment(1)
Digging a bit deeper, printing self.field_name inside DRF's Field class, the asset_location is never printed.... So I think it is not being initialized correctly...Steamship
S
3

So, after digging deep into the code, the field.get_value() for a nested serializer actually calls html.parse_html_dict() from rest_framework.html, which only validates fields with dotted notation.

In short, i needed to send fields this way: asset_location.x asset_location.y

So I just sent JSON, and changed my Parser to change the JSON fields to the above format, this is how my parser looks now:

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)
Steamship answered 29/7, 2021 at 6:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.