vitalik / django-ninja

💨 Fast, Async-ready, Openapi, type hints based framework for building APIs
https://django-ninja.dev
MIT License
6.59k stars 396 forks source link

Django Ninja + GeoDjango #335

Open Jonesus opened 2 years ago

Jonesus commented 2 years ago

Currently using GeoDjango's custom fields breaks Django ninja with the following error:

Exception in thread django-main-thread:
...
< Omitted for brevity >
...
File "/opt/pysetup/.venv/lib/python3.9/site-packages/ninja/orm/fields.py", line 121, in get_schema_field
   python_type = TYPES[internal_type]
KeyError: 'PointField'

Minimal reproduction models:

models.py

from django.contrib.gis.db import models
from ninja import ModelSchema

class Restaurant(models.Model):
    location = models.PointField()

class RestaurantOut(ModelSchema):
    class Config:
        model = Restaurant
        model_fields = ["location"]

and urls:

urls.py

from django.urls import path
from ninja import NinjaAPI
from .models import Restaurant, RestaurantOut

api = NinjaAPI()

@api.get("/restaurants", response=list[RestaurantOut])
def list_restaurants(request):
    return Restaurant.objects.all()

urlpatterns = [path("api/", api.urls)]

Is GeoDjango support something that could be considered to be included in the project? Would using another package like geojson-pydantic help?

Jonesus commented 2 years ago

I managed to work around this issue for now by referring to https://github.com/vitalik/django-ninja/issues/53, and using a custom property on the model:

import json
from django.contrib.gis.db import models
from geojson_pydantic import Point
from ninja import ModelSchema

class Restaurant(models.Model):
    name = models.CharField()
    location = models.PointField()

    @property
    def location_geometry(self):
        return json.loads(self.location.json)

class RestaurantOut(ModelSchema):
    location_geometry: Point

    class Config:
        model = Restaurant
        model_fields = ["name"]

However, it would be nice to have proper support for the GeoDjango functionalities: I tried some hacking around django-ninja source, and by changing /ninja/orm/fields.py:

from geojson_pydantic import Point 

...

TYPES = {
    ...
    "PointField": Point
}

I was first able to get the OpenAPI docs to work properly, and then by changing /ninja/schema.py:

import json
from django.contrib.gis.geos import Point

...

class DjangoGetter(GetterDict):
    def get(self, key: Any, default: Any = None) -> Any:
        ...
        elif isinstance(result, Point):
            return json.loads(result.json)

I got the endpoint working too. I further noted that overloading a getter_dict in ModelSchema -> Config with a minimal custom Getter-class worked also.

Now I'm not sure how do you feel about supporting all the GeoDjango functionalities natively, these workarounds probably work fine for me, but personally I would of course prefer to have all these features supported automagically, like with rest of this great framework :) Also having django-ninja depend on geojson-pydantic might not be something that you want.

To reduce some of this boilerplate (and also pave way for some completely different django field packages) maybe exposing the TYPES mapping somehow could be considered? If one was able to extend it with third party validators and fields it would ease the development flow, manually overloading getter_dict works fine for me and I've yet to come up with a less hacky way of transforming a django.contrib.gis.geos.Point to a dict other than dumping and parsing JSON :smile:

demiurg commented 2 years ago

Thanks for posting this workaround, I found it extremely useful. I think since GeoDjango is a django contrib, it seems like something django-ninja should probably support.

Kazade commented 1 year ago

I've also experienced the same issue but a custom field with a custom internal type (long story...) so it would be good if there was a mechanism for registering custom fields and their counterpart Python type.

xncbf commented 8 months ago

It's a temporary solution, but I was able to get it to work without hacking by putting this code in django settings.py.

from ninja.orm import fields
from geojson_pydantic import Point 

fields.TYPES.update({"PointField": Point})