vitalik / django-ninja

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

Hinting ModelSchema about custom fields #704

Open jleclanche opened 1 year ago

jleclanche commented 1 year ago

I use prefixed UUIDv7 as ID fields in my database. The field looks like this:

from django.db.models import UUIDField
from uuid6 import uuid7
from uuid import UUID
from base58 import b58encode, b58decode

class PrefixedIdentityField(UUIDField):
    def __init__(self, prefix: str, *args, **kwargs):
        self.prefix = prefix
        kwargs.setdefault("default", uuid7)
        kwargs.setdefault("primary_key", True)
        kwargs.setdefault("editable", False)
        super().__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        del kwargs["default"]
        kwargs["prefix"] = self.prefix
        return name, path, args, kwargs

    def to_python(self, value: UUID | str) -> UUID:
        if isinstance(value, UUID):
            return value
        value = value.rpartition("_")[-1]
        return UUID(bytes=b58decode(value))

    def from_db_value(self, value: UUID | str, expression, connection) -> str:
        if isinstance(value, str):
            value = UUID(value)
        return self.prefix + b58encode(value.bytes).decode()

UUIDv7 is great. It's lexicographically sortable, and the prefix means I can distinguish one ID from another and have a created timestamp without storing extra information in the db. So the idea is that the API returns IDs such as org_BwFJbmSZpBC3YZXUm3jjG, instead of 0186de4c-f634-7baa-9a1f-d77dbe2296eb.

But when I give a model with that field to python, I get this pydantic error:

1 validation error for TokenSchema
user -> organizations -> 0 -> id
  value is not a valid uuid (type=type_error.uuid)
Traceback (most recent call last):
  File "/home/adys/.cache/pypoetry/virtualenvs/financica-AU4l5g1E-py3.10/lib/python3.10/site-packages/ninja/operation.py", line 104, in run
    result = self.view_func(request, **values)
  File "/home/adys/src/financica/backend/financica/api/auth.py", line 31, in obtain_token
    return TokenSchema(token=token.secret, user=token.user)
  File "pydantic/main.py", line 341, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for TokenSchema
user -> organizations -> 0 -> id
  value is not a valid uuid (type=type_error.uuid)
1 validation error for TokenSchema
user -> organizations -> 0 -> id
  value is not a valid uuid (type=type_error.uuid)
Traceback (most recent call last):
  File "/home/adys/.cache/pypoetry/virtualenvs/financica-AU4l5g1E-py3.10/lib/python3.10/site-packages/ninja/operation.py", line 104, in run
    result = self.view_func(request, **values)
  File "/home/adys/src/financica/backend/financica/api/auth.py", line 31, in obtain_token
    return TokenSchema(token=token.secret, user=token.user)
  File "pydantic/main.py", line 341, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for TokenSchema
user -> organizations -> 0 -> id
  value is not a valid uuid (type=type_error.uuid)

Ideally, Django Ninja should be able to tell what the actual type should be, thanks to the from_db_value type hint. Failing that, is there an easier way to hint at ModelSchema what the type is supposed to be, that doesn't involve adding a custom field everywhere it's missing?

vitalik commented 1 year ago

@jleclanche

ok, will think about it - PRs/hints are welcome :)

BTW for now you can override TYPES with your own types fallbackes - see example here https://github.com/vitalik/django-ninja/issues/694#issuecomment-1456543192