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
- Declare the field with geography=True: geom = PolygonField(srid=4326, geography=True).
- Use a projected SRID (e.g. 3857, UTM) so Perimeter is planar in metres.
- Apply Transform to a projected CRS in the query: Perimeter(Transform('geom', 3857)).
- 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
- Default geometry fields to geography=True on PostGIS when you compute geodesic measures.
- Use a projected SRID if you need planar perimeters.
- Add migrations to switch existing columns to geography when needed.
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
- Perimeter cannot use a non-projected field.
- PostGIS only supports geography columns with an SRID of 4326
- Only numeric values of degree units are allowed on geographi
- Area on geodetic coordinate systems not supported.
- This backend doesn't support Length on geodetic fields
AI-assisted analysis of django/django@ae25a40be0 (2026-08-06).
Data as JSON: /api/errors/7a3b889c33c4f00b.
Report an issue: GitHub.