How to handle a PATCH request for a nested Serializer?

Viewed 2317

I am trying to build an endpoint using Django Rest Framework for the device detail endpoint on http://127.0.0.1:8000/api/v1/controller/device/<pk>/

Models:-

Device Model

class AbstractDevice(OrgMixin, BaseModel):
    name = models.CharField()
    mac_address = models.CharField()
    key = KeyField()
    model = models.CharField()
    os = models.CharField()
    system = models.CharField()
    notes = models.TextField(blank=True, help_text=_('internal notes'))
    last_ip = models.GenericIPAddressField()
    management_ip = models.GenericIPAddressField()
    hardware_id = models.CharField()

Config Model

class AbstractConfig(BaseConfig):
    device = models.OneToOneField(Device, on_delete=models.CASCADE)
    templates = SortedManyToManyField()
    vpn = models.ManyToManyField()
    STATUS = Choices('modified', 'applied', 'error')
    status = StatusField()
    context = JSONField()

For the above models, I have created serializers as:-

DeviceConfigSerializer

class DeviceConfigSerializer(serializers.ModelSerializer):
    config = serializers.JSONField()
    context = serializers.JSONField()

    class Meta:
        model = Config
        fields = ['backend', 'status', 'templates', 'context', 'config']

DevicedetailSerializer

class DeviceDetailSerializer(serializers.ModelSerializer):
    config = DeviceConfigSerializer()

    class Meta(BaseMeta):
        model = Device
        fields = [
            'id',
            'name',
            'organization',
            'mac_address',
            'key',
            'last_ip',
            'management_ip',
            'model',
            'os',
            'system',
            'notes',
            'config',
        ]
    def update(self, instance, validated_data):
        instance = self.instance or self.Meta.model(**validated_data)
        instance.name = validated_data['name']
        instance.organization = validated_data['organization']
        instance.mac_address = validated_data['mac_address']
        instance.key = validated_data['key']
        instance.last_ip = validated_data['last_ip']
        instance.management_ip = validated_data['management_ip']
        instance.model = validated_data['model']
        instance.os = validated_data['os']
        instance.system = validated_data['system']
        instance.notes = validated_data['notes']
        instance.config.backend = validated_data['config']['backend']
        instance.config.status = validated_data['config']['status']

        config_templates = validated_data['config']['templates']
        instance.config.templates.clear()
        for template in config_templates:
            instance.config.templates.add(template.pk)

        instance.config.context = json.loads(
            json.dumps(validated_data['config']['context']),
            object_pairs_hook=collections.OrderedDict,
        )
        instance.config.config = json.loads(
            json.dumps(validated_data['config']['config']),
            object_pairs_hook=collections.OrderedDict,
        )
        instance.save()
        instance.config.save()
        return instance

Since I want to incorporate a nested serializer and to make it writable so, it is required to manually add the .update method.

and the views:

Views

class DeviceDetailView(RetrieveUpdateDestroyAPIView):
    serializer_class = DeviceDetailSerializer
    queryset = Device.objects.all()

The above codes work fine for PUT request but when I try to send a patch request, it expects all the fields, i., until I feed all the fields, I am not able to send the request, but then this is not a patch request when I have to feed all the fields for changing a single field.

ps: I have abstracted the representation of the models for this questions, and tried to give an idea of the model.

4 Answers
  1. for your first doubt patch request expects all the fields:

when you are writing instance.organization = validated_data['organization'] you are not checking if the key is actually present or not. so, for that you can do it like validated_data.get('organization'). This represents if you have organization present in validated data then you your variable will be assigned.

you are fetching all the validated data into variables and then you are trying to update it but you can do it easily with this code.

instance = super().update(instance, validated_data)
return instance

This will only update yourfields which is only present in validated_data. It will not update the other fields.

  1. for the second doubt you also want to update the nested serializer fields

you can just get the data from the validated_data and update it.

In this case what you have done is correct and i think it will work perfect.

If you still face any issue, please reply me in this thread i will try to give you answer.

partial will work here. So it would be like

s = DeviceDetailSerializer(instance, data={'name': 'Manish'}, partial=True)
s.is_valid()
s.save()

If you are hitting the URL with the patch request, it will automatically pass the partial argument.

The only issue I found with your code is

  1. Why you are doing self.Meta.model(**validated_data)? because in update method self.instance would always be there
  2. When you are doing instance.organization = validated_data['organization'], you are not checking whether the key is actually present in validated_data or not. So you need to check first whether the key is present or not and if the key is present only then update the data. For that, you can do if 'organization' in validated_data then do instance.organization = validated_data['organization']

Everything else looks good, If you still face any issue, please update the description with that issue for better understanding.

You can override patch method with partial=True

class DeviceDetailView(RetrieveUpdateDestroyAPIView):
    serializer_class = DeviceDetailSerializer
    queryset = Device.objects.all()
    
    def patch(self, request, *args, **kwargs):
        kwargs['partial'] = True 
        return self.update(request, *args, **kwargs)

or you can try this method, for this you have to write patch manually https://www.django-rest-framework.org/api-guide/serializers/#partial-updates

# Update `comment` with partial data
serializer = CommentSerializer(comment, data={'content': 'foo bar'}, partial=True)
Related