Skip to main content

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.

Python Django django-ninja pydantic License: MIT

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, LineString minimum 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

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.
  • RasterField is out of scope, since rasters are not GeoJSON geometries.
  • Feature and FeatureCollection wrappers 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:

  1. On pypi.org (and test.pypi.org) add a Trusted Publisher for owner sirmmo, repo django-ninja-spatial, workflow publish.yml, environment pypi (and testpypi).
  2. Create the matching GitHub Environments named pypi and testpypi.

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_ninja_spatial-0.9.1.tar.gz (23.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_ninja_spatial-0.9.1-py3-none-any.whl (20.2 kB view details)

Uploaded Python 3

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

Hashes for django_ninja_spatial-0.9.1.tar.gz
Algorithm Hash digest
SHA256 19f87e4d678fe3ae5bac9e5e1994994ea83bbe0d854f4fce054285acca823081
MD5 e3b9450db552175d06a056fb713c0e10
BLAKE2b-256 7ab4a279d143fcc81bf062dd6c28042645b6520cef128a375d97a4bbc1807d23

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_ninja_spatial-0.9.1.tar.gz:

Publisher: publish.yml on sirmmo/django-ninja-spatial

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_ninja_spatial-0.9.1-py3-none-any.whl.

File metadata

File hashes

Hashes for django_ninja_spatial-0.9.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9f9cfecd522cc81f72e0eef93284d24f79f93850908b5f0ec47c5480e58d3950
MD5 e7472ee2dbd364d4152c3eb5f287f195
BLAKE2b-256 4613c03c280fb547e0867a079c5567d0f1af216e8478bca9e3308f1f0431ee71

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page