Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create a nested serializer with Django Rest Framework, but without primary keys as identity

I have two models to expose via an API: every RegionValue has a ForeignKey to a MapAnswer. I wish to represent this in our API built using rest_framework by making the RegionValues a field inside the MapAnswer endpoint. My rest_framework serializers looks like this:

class RegionValueSerializer(serializers.ModelSerializer):

    class Meta:
        model = RegionValue
        fields = ('region_id', 'value')

class MapAnswerSerializer(serializers.ModelSerializer):
    regionvalue_set = RegionValueSerializer(many=True, allow_add_remove=True, required=False)
    declined = serializers.BooleanField(required=False)

    class Meta:
        model = MapAnswer
        fields = ('declined', 'regionvalue_set')

This works fine from a read perspective, but updating the regionvalue_set has an issue where new RegionValues are always created instead of linking to an existing RegionValue. If I include 'id' in the fields of RegionValueSerializer then it solves this problem, but I'd prefer not to expose the primary key! The RegionValues are uniquely determined by the their region_id and the MapAnswer they are associated with.

like image 501
ferrouswheel Avatar asked Nov 19 '25 20:11

ferrouswheel


1 Answers

The way I solved this required customising the RegionValueSerializer, intercepting the conversion from native python data types to the field.

class RegionValueSerializer(serializers.ModelSerializer):

    def field_from_native(self, data, files, field_name, into):
        # We need to check all the data items, and ensure they
        # are matched to an existing primary id if they already
        # present

        # Returns nothing because this method mutates 'into'
        super(RegionValueSerializer, self).field_from_native(data, files, field_name, into)

        map_answer = self.parent.object
        new_into = []
        for rv in into.get('regionvalue_set'):
            if rv.id is None:
                try:
                    existing_rv = RegionValue.objects.get(answer=map_answer, region_id=rv.region_id)
                    existing_rv.value = rv.value
                    rv = existing_rv
                except RegionValue.DoesNotExist:
                    pass
            new_into.append(rv)
        into['regionvalue_set'] = new_into

    def get_identity(self, data):
        try:
            # Technically identity is defined by region_id AND self.parent.object.id,
            # but we assume that RegionValueSerializer will only ever be used as a
            # field that is part of MapAnswerSerializer.
            return data.get('region_id', None)
        except AttributeError:
            return None

Caveats: Note that some of these methods are not really discussed in rest_framework's docs, so I'm not sure how stable this will be. Also, this solution hits the database more than really necessary (the lookup for existing values is duplicating lookups that occur in the parent Serializer).

like image 82
ferrouswheel Avatar answered Nov 21 '25 09:11

ferrouswheel



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!