GeoJSON serialization/deserialization of geospatial geometries for django-ninja (GeoDjango / GEOS).
Project description
django-ninja-spatial
GeoJSON and Mapbox Vector Tile serialization for django-ninja with GeoDjango.
It serializes GeoDjango geometry fields to RFC 7946
GeoJSON and validates GeoJSON back into GEOSGeometry, both in hand-written Schema
classes and in ModelSchema. Point a ModelSchema at a model with geometry fields and
they round-trip as GeoJSON:
from ninja import NinjaAPI, ModelSchema
from ninja_spatial import register_geometry_fields
from places.models import Place # location = models.PointField()
register_geometry_fields() # once, at startup
class PlaceSchema(ModelSchema):
class Meta:
model = Place
fields = "__all__"
// GET /api/places/1
{
"id": 1,
"name": "Duomo di Milano",
"location": { "type": "Point", "coordinates": [9.1916, 45.4641] }
}
Features
- Covers all seven GeoJSON geometry types (Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, GeometryCollection) plus a generic geometry type that accepts any of them.
- Works in both directions: GeoJSON requests are validated into
GEOSGeometry, and geometry fields are serialized to GeoJSON in responses. - Validates per RFC 7946 (position length,
LineStringminimum points, closed linear rings), so you get useful pydantic errors and an accurate OpenAPI schema. - Encodes and decodes Mapbox Vector Tiles, with helpers for XYZ tiling.
- Converts between GEOS and GeoJSON without going through GDAL, which keeps Z coordinates and empty geometries intact.
- The GeoJSON schemas depend only on pydantic. GeoDjango and MVT support are optional.
- Built on pydantic v2 and ships type hints (
py.typed).
Table of contents
- Installation
- Quick start
- Field types
- Usage
- Example project
- Compatibility
- How it works
- Notes and limitations
- Releasing
- Contributing
- License
Installation
pip install django-ninja-spatial # core (pydantic only)
pip install "django-ninja-spatial[ninja]" # + Django and django-ninja (GeoDjango bridge)
pip install "django-ninja-spatial[mvt]" # + Mapbox Vector Tile support (shapely)
You also need GeoDjango itself (GEOS, plus GDAL/PROJ for a spatial database backend). See the GeoDjango install guide.
Quick start
Register the geometry field types once, for example in your app's AppConfig.ready:
# apps.py
from django.apps import AppConfig
from ninja_spatial import register_geometry_fields
class PlacesConfig(AppConfig):
name = "places"
def ready(self):
register_geometry_fields()
# models.py
from django.contrib.gis.db import models
class Place(models.Model):
name = models.CharField(max_length=100)
location = models.PointField()
area = models.PolygonField(null=True, blank=True)
# api.py
from ninja import NinjaAPI, ModelSchema
from places.models import Place
api = NinjaAPI()
class PlaceSchema(ModelSchema):
class Meta:
model = Place
fields = "__all__"
@api.get("/places/{place_id}", response=PlaceSchema)
def get_place(request, place_id: int):
return Place.objects.get(pk=place_id)
Geometry columns now serialize to GeoJSON, and the OpenAPI schema describes them.
Field types
These annotated types (importable from ninja_spatial) go in any Schema. They validate
input GeoJSON or WKT into a GEOSGeometry and serialize a GEOSGeometry back to a GeoJSON
object.
| Field | Accepts / emits | GeoDjango model field it maps to |
|---|---|---|
GeometryField |
any of the seven types | GeometryField |
PointField |
Point |
PointField |
LineStringField |
LineString |
LineStringField |
PolygonField |
Polygon |
PolygonField |
MultiPointField |
MultiPoint |
MultiPointField |
MultiLineStringField |
MultiLineString |
MultiLineStringField |
MultiPolygonField |
MultiPolygon |
MultiPolygonField |
GeometryCollectionField |
GeometryCollection |
GeometryCollectionField |
Each one accepts a GeoJSON object (dict), a GeoJSON / WKT / EWKT / HEXEWKB string, an
existing GEOSGeometry, or a ninja_spatial.geojson schema instance. A typed field
returns a 422 when given the wrong geometry type.
Usage
ModelSchema
After calling register_geometry_fields(), ModelSchema introspects GeoDjango geometry
columns on its own (see Quick start). There is nothing else to wire up.
Explicit Schema fields
Write schemas by hand when you want full control. On input, fields validate GeoJSON and
produce a GEOSGeometry; on output, they serialize a GEOSGeometry to a GeoJSON object.
from typing import Optional
from ninja import NinjaAPI, Schema
from ninja_spatial import PointField, PolygonField, GeometryField
api = NinjaAPI()
class PlaceIn(Schema):
name: str
location: PointField # must be a Point
area: Optional[PolygonField] = None # nullable, so wrap in Optional
anything: Optional[GeometryField] = None # any of the seven geometry types
@api.post("/places")
def create_place(request, payload: PlaceIn):
# payload.location is a django.contrib.gis.geos.Point (srid 4326)
place = Place.objects.create(name=payload.name, location=payload.location)
return {"id": place.id}
Declare nullable geometry fields as Optional[...] (for example
Optional[PointField] = None). A bare PointField = None is not optional at the type
level and will reject null. For a non-default SRID, use the factory:
geometry_field(geom_types=("Point",), srid=3857).
Pure-Pydantic GeoJSON schemas (without GeoDjango)
The ninja_spatial.geojson module is plain pydantic v2 and needs no GeoDjango:
from ninja_spatial.geojson import Point, Geometry, parse_geometry
Point.model_validate({"type": "Point", "coordinates": [1.0, 2.0]})
# Geometry is a discriminated union over the "type" member (the generic geometry type):
geom = parse_geometry({"type": "LineString", "coordinates": [[0, 0], [1, 1]]})
type(geom).__name__ # "LineString"
from ninja import Schema
from ninja_spatial.geojson import Geometry
class Shape(Schema):
geometry: Geometry
Conversion helpers
from ninja_spatial import geos_to_geojson, geojson_to_geos
geom = geojson_to_geos({"type": "Point", "coordinates": [1, 2]}) # GEOSGeometry, srid 4326
geos_to_geojson(geom) # {"type": "Point", ...}
geos_to_geojson(geom, precision=6) # round coordinates
geojson_to_geos(value, srid=...) defaults to srid=4326 (the GeoJSON CRS); pass None
to leave it unset. The schemas also provide schema.to_geos(srid=...) and
Model.from_geos(geom, precision=...).
Mapbox Vector Tiles
Install the extra (it brings in shapely): pip install "django-ninja-spatial[mvt]".
encode_tile reprojects your geometries (SRID 4326 by default) to Web Mercator and
quantizes them to the requested tile:
from django.http import HttpResponse
from ninja_spatial import mvt
from places.models import Place
@api.get("/tiles/{int:z}/{int:x}/{int:y}")
def tile(request, z: int, x: int, y: int):
minx, miny, maxx, maxy = mvt.tile_bounds(z, x, y) # EPSG:3857
rows = Place.objects.all() # filter to the bbox in prod
data = mvt.encode_tile(
[{"name": "places",
"features": [{"geometry": p.location,
"properties": {"id": p.id, "name": p.name}} for p in rows]}],
z, x, y,
)
return HttpResponse(data, content_type="application/vnd.mapbox-vector-tile")
For geometries that are already in tile or local coordinates you can encode directly, with no GeoDjango involved:
tile = mvt.encode_layer(
"places",
[{"geometry": {"type": "Point", "coordinates": [50, 50]}, "properties": {"id": 1}}],
quantize_bounds=(0, 0, 100, 100), extent=4096,
)
mvt.decode(tile) # {"places": {"features": [...]}}
Geometries accept the same forms as elsewhere. MVT covers Point, LineString, Polygon and
their Multi variants; GeometryCollection is not an MVT geometry.
Example project
There is a complete GeoDjango and django-ninja app in example/: GeoJSON CRUD,
bounding-box and nearest-neighbour queries, point-in-polygon, and a vector-tile endpoint,
all running on SpatiaLite so it needs no PostGIS server.
cd example
pip install -r requirements.txt
python manage.py migrate && python manage.py seed
python manage.py runserver # interactive docs at http://127.0.0.1:8000/api/docs
Compatibility
| Supported | |
|---|---|
| Python | 3.8 to 3.12 |
| Django | 4.2 to 5.x |
| django-ninja | 1.0+ |
| pydantic | 2.x |
| Databases | any GeoDjango spatial backend (PostGIS, SpatiaLite, and so on) |
The pure GeoJSON schemas (ninja_spatial.geojson) require only pydantic; GeoDjango and the
MVT extra are optional.
How it works
The ninja_spatial.geojson module holds a pydantic v2 model for each geometry type and a
type-discriminated Geometry union, validated against RFC 7946. The ninja_spatial.fields
module exposes annotated types whose __get_pydantic_core_schema__ validates input into a
GEOSGeometry and serializes it back to GeoJSON, while __get_pydantic_json_schema__ keeps
the OpenAPI documentation accurate. register_geometry_fields() registers each GeoDjango
field's get_internal_type() with django-ninja's ORM type registry so ModelSchema
introspection picks them up. The GEOS-to-GeoJSON conversion is written against GEOS
constructors and .tuple rather than GDAL, which is what preserves Z and empty geometries.
Notes and limitations
- GeoJSON coordinates are WGS 84 (lon, lat, optional altitude). If a column uses another
SRID, set it with
geometry_field(srid=...)or reproject before assigning. - GEOS does not support nested
GeometryCollections (RFC 7946 discourages them too), so converting one to GEOS raises an error. RasterFieldis out of scope, since rasters are not GeoJSON geometries.FeatureandFeatureCollectionwrappers are out of scope; this library handles geometry objects, which is what model fields store.
Releasing
Releases go to PyPI through .github/workflows/publish.yml
using PyPI Trusted Publishing, so no API tokens
are stored in the repo.
One-time setup:
- On pypi.org (and
test.pypi.org) add a Trusted
Publisher for owner
sirmmo, repodjango-ninja-spatial, workflowpublish.yml, environmentpypi(andtestpypi). - Create the matching GitHub Environments named
pypiandtestpypi.
To cut a release, bump version in pyproject.toml, push, and publish a GitHub Release
tagged vX.Y.Z (the workflow checks the tag against the version). To rehearse against
TestPyPI, run the workflow manually with the target set to testpypi.
Contributing
Issues and pull requests are welcome.
git clone https://github.com/sirmmo/django-ninja-spatial
cd django-ninja-spatial
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest
The test suite configures Django in tests/conftest.py and runs in memory, with no spatial
database required.
License
MIT © Marco Montanari
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_ninja_spatial-0.9.1.tar.gz.
File metadata
- Download URL: django_ninja_spatial-0.9.1.tar.gz
- Upload date:
- Size: 23.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
19f87e4d678fe3ae5bac9e5e1994994ea83bbe0d854f4fce054285acca823081
|
|
| MD5 |
e3b9450db552175d06a056fb713c0e10
|
|
| BLAKE2b-256 |
7ab4a279d143fcc81bf062dd6c28042645b6520cef128a375d97a4bbc1807d23
|
Provenance
The following attestation bundles were made for django_ninja_spatial-0.9.1.tar.gz:
Publisher:
publish.yml on sirmmo/django-ninja-spatial
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_ninja_spatial-0.9.1.tar.gz -
Subject digest:
19f87e4d678fe3ae5bac9e5e1994994ea83bbe0d854f4fce054285acca823081 - Sigstore transparency entry: 1629076742
- Sigstore integration time:
-
Permalink:
sirmmo/django-ninja-spatial@9d891915d0042acab7eef55586d471a47ef9882c -
Branch / Tag:
refs/tags/v0.9.1 - Owner: https://github.com/sirmmo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9d891915d0042acab7eef55586d471a47ef9882c -
Trigger Event:
release
-
Statement type:
File details
Details for the file django_ninja_spatial-0.9.1-py3-none-any.whl.
File metadata
- Download URL: django_ninja_spatial-0.9.1-py3-none-any.whl
- Upload date:
- Size: 20.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9f9cfecd522cc81f72e0eef93284d24f79f93850908b5f0ec47c5480e58d3950
|
|
| MD5 |
e7472ee2dbd364d4152c3eb5f287f195
|
|
| BLAKE2b-256 |
4613c03c280fb547e0867a079c5567d0f1af216e8478bca9e3308f1f0431ee71
|
Provenance
The following attestation bundles were made for django_ninja_spatial-0.9.1-py3-none-any.whl:
Publisher:
publish.yml on sirmmo/django-ninja-spatial
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_ninja_spatial-0.9.1-py3-none-any.whl -
Subject digest:
9f9cfecd522cc81f72e0eef93284d24f79f93850908b5f0ec47c5480e58d3950 - Sigstore transparency entry: 1629076753
- Sigstore integration time:
-
Permalink:
sirmmo/django-ninja-spatial@9d891915d0042acab7eef55586d471a47ef9882c -
Branch / Tag:
refs/tags/v0.9.1 - Owner: https://github.com/sirmmo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9d891915d0042acab7eef55586d471a47ef9882c -
Trigger Event:
release
-
Statement type: