Skip to content

Validation of geometry type #296

Open
@pjpetersik

Description

@pjpetersik

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.

field_mapping.update(
{
models.GeometryField: GeometryField,
models.PointField: GeometryField,
models.LineStringField: GeometryField,
models.PolygonField: GeometryField,
models.MultiPointField: GeometryField,
models.MultiLineStringField: GeometryField,
models.MultiPolygonField: GeometryField,
models.GeometryCollectionField: GeometryField,
}
)

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 :).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions