nunolourenco
nunolourenco

Reputation: 699

Django Custom Deserialization

I have the following django model:

class Person(models.Model):
    name = models.CharField()
    location = models.PointField()

And I want to create a serialiser/deserialiser for this model. However the JSON object hat I receive is the following:

{
   "userList":[
      {
         "username": "Foo",
         "lat":40.875736,
         "lon":8.94382834,
      },
      {
      "username": "Bar",
      "lat":40.875736,
      "lon":8.94382834,
      }, 
   ]
}

Serialiser

class PersonListSerializer(serializers.PersonSerializer):
    username = serializers.CharField()
    lat = serializers.FloatField()
    lon = serializers.FloatField()


class PersonSerializer(serializers.ModelSerializer):
    personList = PersonListSerializer

    class Meta:
        model = Person

Is it possible to create a custom serializer/deserialiser to deal with this structure without having to create an additional model (PersonList)?

Thanks in advance.

Upvotes: 2

Views: 1820

Answers (1)

dhke
dhke

Reputation: 15408

Well, this took a while and it was definitely a nice learning experience.

Your problem can be split into two separate ones:

  1. You need a field that takes its value from two separate fields in the representation dict and also outputs two separate values. I chickened out on the former one and did a custom to_internal_value()
  2. You need a ListSerializer that does not accept and return a list, but a dictionary with a single nested field that contains the actual list.

You can do it without touching the model at all.

class ListDictSerializer(serializers.ListSerializer):
    def get_field_name(self):
        if not hasattr(self.Meta, 'field_name'):
            raise ValueError('ListDictSerializer requires defining Meta.field_name overriding get_field_name()')
        return self.Meta.field_name

    def to_internal_value(self, data):
        field_name = self.get_field_name()
        return super(ListDictSerializer, self).to_internal_value(data[field_name])

    def to_representation(self, data):
        field_name = self.get_field_name()
        return ReturnDict({
                field_name: super(ListDictSerializer, self).to_representation(data)
            }, serializer=self
        )

    @property
    def data(self):
       # skip over the ListSerializer to get the real data without the
        # ReturnList
        ret = super(serializers.ListSerializer, self).data
        return ReturnDict(ret, serializer=self)


class PersonListSerializer(ListDictSerializer):
    class Meta:
        field_name = 'userList'


class PersonSerializer(serializers.ModelSerializer):
    class Meta:
        list_serializer_class = PersonListSerializer
        model = Person
        fields = ('username', 'lat', 'lon')

    username = serializers.CharField(source='name')
    lat = serializers.SerializerMethodField(method_name='get_latitude')
    lon = serializers.SerializerMethodField(method_name='get_longitude')

    def get_latitude(self, instance):
        return instance.location.coords[1]

    def get_longitude(self, instance):
        return instance.location.coords[0]

    def to_internal_value(self, data):
        return ReturnDict({
            'name': data.get('username'),
            'location': Point(data['lat'], data['lon']),
        }, serializer=self)

Note that DRF allows you to export arbitrary model properties (not only fields) read/write. That is, we could get away with defining @property lat and lon on the model with appropriate getters and setters. However, geos objects like Point are immutable after creation, so you cannot selectively set a single coordinate on an existing object.

Upvotes: 5

Related Questions