How can I flatten a foreignkey object with django-rest-framework-(gis)

1.9k Views Asked by At

I have searched long and far for a solution that is up to date and specific to my problem but have yet not found a solution or a clear documentation on what I really need to do in order to flatten a relationship to become geojson compliant.

This question is almost identical to mine, however the solutions or answers does not solve the problem and still produces invalid GeoJSON.

Related

Problem

I have a Location model that holds a pointfield with SRID=4326. I also have a TrainStation model that has location field as foreign key to Location. When I serialize the TrainStation via the GeoFeatureModelSerializer it produces invalid GeoJSON (see example below "invalid geojson").

(Valid GeoJSON can, of course, be obtained if I where to store the PointField in the TrainStation model, but in my case, I cannot do that so I need to flatten it somehow.)

Question

  • How do I achieve output like the "Valid GeoJSON" example below?

Research

I am a newcomer to both Python and Django thus I am yet not very good at reading other people's source code, however I think I can conclude that I need to somehow override the to_representation() method in order to get what I want, but my searches are so far fruitless so I am stuck.

models.py

class Location(models.Model):

    point = models.PointField()

class TrainStation(models.Model):

    location_signature = models.CharField(primary_key=True, max_length=32)
    advertised_location_name = models.CharField(max_length=32)
    country_code = models.ForeignKey(Country)
    county_no = models.ForeignKey(County)
    location = models.ForeignKey(Location, null=True)

serializers.py

class LocationSerializer(ModelSerializer):

    class Meta:
        model = Location
        geo_field = 'point'
        fields = [
            'point',
        ]


class TrainStationSerializer(GeoFeatureModelSerializer):

    location_signature = PrimaryKeyRelatedField(read_only=True)
    location = LocationSerializer(read_only=True)
    country_code = StringRelatedField(read_only=True)
    county_no = StringRelatedField(read_only=True)

    class Meta:
        model = TrainStation
        geo_field = 'location'
        fields = [
            'location_signature',
            'advertised_location_name',
            'country_code',
            'county_no',
        ]

GeoJSON Output examples:

I have verified the output on http://geojson.io to determine if its valid or not.

Invalid GeoJSON

{
    "type": "FeatureCollection",
    "features": [
        {
            "id": "Ak",
            "type": "Feature",
            "geometry": {
                "point": {           <------+------ offending lines
                    "type": "Point",        |
                    "coordinates": [        |
                        18.8303462142963,   |
                        68.3486410812835    |
                    ]                       |
                }                    <------+
            },
            "properties": {
                "advertised_location_name": "Abisko Östra",
                "country_code": "Sverige",
                "county_no": "Norrbottens län"
            }
        }
    ]
}

Valid GeoJSON

This is the output I am looking for.

{
    "type": "FeatureCollection",
    "features": [
        {
            "id": "Ak",
            "type": "Feature",
            "geometry": {
                "type": "Point",
                "coordinates": [
                    18.8303462142963,
                    68.3486410812835
                ]
            },
            "properties": {
                "advertised_location_name": "Abisko Östra",
                "country_code": "Sverige",
                "county_no": "Norrbottens län"
            }
        }
    ]
}
2

There are 2 best solutions below

0
On

I think the problem might be that you're declaring a whole ModelSerializer as your geo_field and its just sticking the result of that serializer, which is itself an entire geojson object, inside the geometry part of your main geojson object and the Serializers that come with django-restframework-gis just don't know what to do about that.

What the GeoFeatureModelSerializer class would like to see as a geo_field is a GeometryField which has been serialized by its own rest_framework_gis.fields.GeometryField. I imagine any of the following would get you the behavior you want.

  1. just add location.point to your TrainStationSerializer as its geofield using the double underscore format. Disclaimer: I'm not actually sure off the top of my head if drf-gis implements the double underscores properly for the geo_field property, but I think this should work.

    from rest_framework_gis.fields import GeometryField
    from rest_framework_gis.serializers import GeoFeatureModelSerializer
    
    
    class TrainStationSerializer(GeoFeatureModelSerializer):
    
        location_signature = PrimaryKeyRelatedField(read_only=True)
        country_code = StringRelatedField(read_only=True)
        county_no = StringRelatedField(read_only=True)
    
        class Meta:
            model = TrainStation
            geo_field = 'location__point'
            fields = [
                'location_signature',
                'advertised_location_name',
                'country_code',
                'county_no',
            ]
    
  2. Use the fields.GeometryField class that comes with drf-gis and specify your location.point field as its source.

    from rest_framework_gis.fields import GeometryField
    from rest_framework_gis.serializers import GeoFeatureModelSerializer
    
    
    class TrainStationSerializer(GeoFeatureModelSerializer):
    
        location_signature = PrimaryKeyRelatedField(read_only=True)
        location = GeometryField(source='location.point')
        country_code = StringRelatedField(read_only=True)
        county_no = StringRelatedField(read_only=True)
    
        class Meta:
            model = TrainStation
            geo_field = 'location'
            fields = [
                'location_signature',
                'advertised_location_name',
                'country_code',
                'county_no',
            ]
    
  3. Use the GeometrySerializerMethodField as shown in the example on drf-gis's readme

    from rest_framework_gis.fields import GeometrySerializerMethodField
    from rest_framework_gis.serializers import GeoFeatureModelSerializer
    
    
    class TrainStationSerializer(GeoFeatureModelSerializer):
    
        location_signature = PrimaryKeyRelatedField(read_only=True)
        location = GeometrySerializerMethodField()
        country_code = StringRelatedField(read_only=True)
        county_no = StringRelatedField(read_only=True)
    
        def get_location(self, obj):
            return obj.location.point
    
        class Meta:
            model = TrainStation
            geo_field = 'location'
            fields = [
                'location_signature',
                'advertised_location_name',
                'country_code',
                'county_no',
            ]
    
0
On

I have now solved this issue with the following code:

class LocationSerializer(ModelSerializer):

    def to_representation(self, obj):

        representation = super().to_representation(obj)
        point_representation = representation.pop('point')
        for key in point_representation:
            representation[key] = point_representation[key]

        return representation

    class Meta:
        model = Location
        geo_field = 'point'
        fields = [
            'point',
        ]

And this does indeed produce valid GeoJSON:

{
    "type": "FeatureCollection",
    "features": [
        {
            "id": "Ak",
            "type": "Feature",
            "geometry": {
                "type": "Point",
                "coordinates": [
                    18.8303462142963,
                    68.3486410812835
                ]
            },
            "properties": {
                "advertised_location_name": "Abisko Östra",
                "country_code": "Sverige",
                "county_no": "Norrbottens län"
            }
        }
    ]
}

If someone has any inputs on this feel free to contribute and add an answer :-)