openwisp / django-rest-framework-gis

Geographic add-ons for Django REST Framework. Maintained by the OpenWISP Project.
http://openwisp.org
MIT License
1.07k stars 200 forks source link

Validation of geometry type #296

Open pjpetersik opened 2 months ago

pjpetersik commented 2 months ago

I currently observe the behavior that a ModelSerializer does not validate if the geometry field is of the right geometry type. Hence, the serializer does not throw a validation error when it receives data of an unexpected geometry type but later runs into a TypeError that is thrown by Django when the data is saved to the database.

Example

# models.py
from django.db import models
from django.contrib.gis.db import models as gis_models

class PolygonModel(models.Model):
    polygon = gis_models.PolygonField()

# serializer.py
from rest_framework import serializers
class PolygonSerializer(serializers.ModelSerializer):
    class Meta:
        model = PolygonModel
        fields = "__all__"

# view.py
from rest_framework import viewsets
class PolygonViewSet(viewsets.ModelViewSet):
    serializer_class = PolygonSerializer
    queryset = PolygonModel.objects.all()

# urls.py
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register("polygons', PolygonViewSet)
urlpatterns = router.urls

If the /polygons end point would receive a POST/PUT request with the following body

{
    "polygon": {
        "type": "Point"
        "geometry": [0, 0]
    }
}

the following error would be raised by Django:

File "/usr/local/lib/python3.10/site-packages/django/db/models/manager.py", line 87, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/usr/local/lib/python3.10/site-packages/django/db/models/query.py", line 656, in create
    obj = self.model(**kwargs)
  File "/usr/local/lib/python3.10/site-packages/django/db/models/base.py", line 546, in __init__
    _setattr(self, field.attname, val)
  File "/usr/local/lib/python3.10/site-packages/django/contrib/gis/db/models/proxy.py", line 76, in __set__
    raise TypeError(
TypeError: Cannot set Plot SpatialProxy (POLYGON) with value of type: <class 'django.contrib.gis.geos.point.Point'>

However, for my opinion a ValidationError would be more appropriated to be raised.

Problem

I think the problem comes done to the field_mapping in the apps.py module which maps the GeoDjango fields to a generic GeometryField.

https://github.com/openwisp/django-rest-framework-gis/blob/4f244d5d8a7ad5b453fd04f64150818d15123e01/rest_framework_gis/apps.py#L24-L35

Solution

My idea would be to append a GeometryFieldValidator to validators list of the GeometryField:

class GeometryValidator:
    def __init__(self, geom_type):
        self.geom_type = geom_type

    def __call__(self, value):
        if value.geom_type != self.geom_type:
            raise serializers.ValidationError("Wrong geometry type provided.", code="invalid")

class GeometryField(Field):
    """
    A field to handle GeoDjango Geometry fields
    """

    type_name = 'GeometryField'

    def __init__(
        self, precision=None, remove_duplicates=False, auto_bbox=False, geom_type=None, **kwargs
    ):
        """
        :param auto_bbox: Whether the GeoJSON object should include a bounding box
        """
        self.precision = precision
        self.auto_bbox = auto_bbox
        self.remove_dupes = remove_duplicates
        super().__init__(**kwargs)
        self.style.setdefault('base_template', 'textarea.html')
        if geom_type:
            self.validators.append(GeometryValidator(geom_type))

I have not thought about the specifics of the implementation. However, I would be open to work on a PR if the described behavior of raising a ValidationError is of interest for the community. Or do you think one should simply solve this by adding the described GeometryValidator manually to each ModelSerializer which contains a geometry field, i.e.:

# serializer.py
from rest_framework import serializers
class PolygonSerializer(serializers.ModelSerializer):
    polygon = GeometryField(validators=[GeometryValidator])
    class Meta:
        model = PolygonModel
        fields = "__all__"

Happy to get your feedback on this :).