django/django · error · NotSupportedError

ST_Perimeter cannot use a non-projected non-geography field.

Error message

ST_Perimeter cannot use a non-projected non-geography field.

What it means

NotSupportedError raised in Perimeter.as_postgresql (functions.py:547) on PostGIS when the field is geodetic AND not geography. ST_Perimeter on a lon/lat geometry column is meaningless in metres; you must use the geography type so PostGIS applies geodesic perimeter.

Source

Thrown at django/contrib/gis/db/models/functions.py:547


class NumGeometries(GeoFunc):
    output_field = IntegerField()
    arity = 1


class NumPoints(GeoFunc):
    output_field = IntegerField()
    arity = 1


class Perimeter(DistanceResultMixin, OracleToleranceMixin, GeoFunc):
    arity = 1

    def as_postgresql(self, compiler, connection, **extra_context):
        function = None
        if self.geo_field.geodetic(connection) and not self.source_is_geography():
            raise NotSupportedError(
                "ST_Perimeter cannot use a non-projected non-geography field."
            )
        dim = min(f.dim for f in self.get_source_fields())
        if dim > 2:
            function = connection.ops.perimeter3d
        return super().as_sql(compiler, connection, function=function, **extra_context)

    def as_sqlite(self, compiler, connection, **extra_context):
        if self.geo_field.geodetic(connection):
            raise NotSupportedError("Perimeter cannot use a non-projected field.")
        return super().as_sql(compiler, connection, **extra_context)


class PointOnSurface(OracleToleranceMixin, GeomOutputGeoFunc):
    arity = 1


class Reverse(GeoFunc):

View on GitHub (pinned to ae25a40be0)

Solutions

  1. Declare the field with geography=True: geom = PolygonField(srid=4326, geography=True).
  2. Use a projected SRID (e.g. 3857, UTM) so Perimeter is planar in metres.
  3. Apply Transform to a projected CRS in the query: Perimeter(Transform('geom', 3857)).
  4. Run a migration to switch the column to geography if switching the model field.

Example fix

# before
geom = models.PolygonField(srid=4326)  # geography=False
qs = Plot.objects.annotate(p=Perimeter('geom'))  # raises
# after
geom = models.PolygonField(srid=4326, geography=True)
# or in-query
qs = Plot.objects.annotate(p=Perimeter(Transform('geom', 3857)))
Defensive patterns

Strategy: validation

Validate before calling

from django.db import connection

def perimeter_ok_on_postgis(field):
    return (not field.geodetic(connection)) or field.geography

if field.geodetic(connection) and not field.geography:
    raise ValueError('Use geography=True or a projected SRID for Perimeter on PostGIS')

Type guard

def geography_or_projected(field, conn) -> bool:
    return (not field.geodetic(conn)) or bool(getattr(field, 'geography', False))

Try / catch

from django.contrib.gis.db.models.functions import Perimeter, Transform
try:
    qs = Model.objects.annotate(p=Perimeter('geom'))
except NotImplementedError:
    qs = Model.objects.annotate(p=Perimeter(Transform('geom', 3857)))

Prevention

When it happens

Trigger: Model.objects.annotate(p=Perimeter('geom')) on PostGIS where geom is a GeometryField with srid=4326 and geography=False.

Common situations: Default GeometryField(srid=4326) and then trying to compute perimeter; converting a PostGIS project from a projected CRS to WGS84 without setting geography=True.

Related errors


AI-assisted analysis of django/django@ae25a40be0 (2026-08-06). Data as JSON: /api/errors/7a3b889c33c4f00b. Report an issue: GitHub.