Repository: vitalik/django-ninja
Pull request #1453 (2025-04-19 07:25:42)

uuidstr url converter instead of shadowing django uuid
Before django ninja shadowed django's uuid to be able to to get string value from url:

```python

@api.get('/some/{uuid:id}')
def some(request, id):
     return {'id': id, 'type': str(type(id))}  # type=str
```

Now added custom converted `uuidstr`

```python
@api.get('/some/{uuidstr:id}')
def some(request, id):
    # id will be str type here
```

django 6.0 will not allow shadowing 


addresses #1452 #1266


Patch:
diff --git a/ninja/router.py b/ninja/router.py
index 6e29b0d..d6f96d0 100644
--- a/ninja/router.py
+++ b/ninja/router.py
@@ -1,3 +1,4 @@
+import re
 from typing import (
     TYPE_CHECKING,
     Any,
@@ -320,6 +321,12 @@ class Router:
         include_in_schema: bool = True,
         openapi_extra: Optional[Dict[str, Any]] = None,
     ) -> None:
+        path = re.sub(r"\{uuid:(\w+)\}", r"{uuidstr:\1}", path, flags=re.IGNORECASE)
+        # django by default convert strings to UUIDs
+        # but we want to keep them as strings to let pydantic handle conversion/validation
+        # if user whants UUID object
+        # uuidstr is custom registered converter
+
         if path not in self.path_operations:
             path_view = PathView()
             self.path_operations[path] = path_view
diff --git a/ninja/signature/details.py b/ninja/signature/details.py
index cb79cf1..607b750 100644
--- a/ninja/signature/details.py
+++ b/ninja/signature/details.py
@@ -289,7 +289,7 @@ def is_pydantic_model(cls: Any) -> bool:
         if get_origin(cls) in UNION_TYPES:
             return any(issubclass(arg, pydantic.BaseModel) for arg in get_args(cls))
         return issubclass(cls, pydantic.BaseModel)
-    except TypeError:
+    except TypeError:  # pragma: no cover
         return False
 
 
diff --git a/ninja/signature/utils.py b/ninja/signature/utils.py
index bc04eac..c3993d8 100644
--- a/ninja/signature/utils.py
+++ b/ninja/signature/utils.py
@@ -72,16 +72,11 @@ def get_args_names(func: Callable[..., Any]) -> List[str]:
     return list(inspect.signature(func).parameters.keys())
 
 
-class NinjaUUIDConverter:
+class UUIDStrConverter(UUIDConverter):
     """Return a path converted UUID as a str instead of the standard UUID"""
 
-    regex = UUIDConverter.regex
+    def to_python(self, value: str) -> str:  # type: ignore
+        return value  # return string value instead of UUID
 
-    def to_python(self, value: str) -> str:
-        return value
 
-    def to_url(self, value: Any) -> str:
-        return str(value)
-
-
-register_converter(NinjaUUIDConverter, "uuid")
+register_converter(UUIDStrConverter, "uuidstr")


Test patch:
diff --git a/tests/main.py b/tests/main.py
index 6535036..38ff7a1 100644
--- a/tests/main.py
+++ b/tests/main.py
@@ -174,8 +174,15 @@ def get_path_param_django_uuid(request, item_id: UUID):
     return item_id
 
 
-@router.get("/path/param-django-uuid-str/{uuid:item_id}")
-def get_path_param_django_uuid_str(request, item_id):
+@router.get("/path/param-django-uuid-notype/{uuid:item_id}")
+def get_path_param_django_uuid_notype(request, item_id):
+    # no type annotation defaults to str..............^
+    assert isinstance(item_id, str)
+    return item_id
+
+
+@router.get("/path/param-django-uuid-typestr/{uuid:item_id}")
+def get_path_param_django_uuid_typestr(request, item_id: str):
     assert isinstance(item_id, str)
     return item_id
 
diff --git a/tests/test_misc.py b/tests/test_misc.py
index 3060a5e..355dc88 100644
--- a/tests/test_misc.py
+++ b/tests/test_misc.py
@@ -7,7 +7,7 @@ from pydantic import BaseModel
 from ninja import NinjaAPI
 from ninja.constants import NOT_SET
 from ninja.signature.details import is_pydantic_model
-from ninja.signature.utils import NinjaUUIDConverter
+from ninja.signature.utils import UUIDStrConverter
 from ninja.testing import TestClient
 
 
@@ -48,7 +48,7 @@ def test_kwargs():
 
 
 def test_uuid_converter():
-    conv = NinjaUUIDConverter()
+    conv = UUIDStrConverter()
     assert isinstance(conv.to_url(uuid.uuid4()), str)
 
 
diff --git a/tests/test_path.py b/tests/test_path.py
index 687d648..e9fea9d 100644
--- a/tests/test_path.py
+++ b/tests/test_path.py
@@ -301,12 +301,17 @@ def test_get_path(path, expected_status, expected_response):
             "31ea378c-c052-4b4c-bf0b-679ce5cfcc2a",
         ),
         (
-            "/path/param-django-uuid/31ea378c-c052-4b4c-bf0b-679ce5cfcc2",
+            "/path/param-django-uuid/31ea378c-c052-4b4c-bf0b-679ce5cfcc2",  # invalid UUID (missing last digit)
             "Cannot resolve",
             Exception,
         ),
         (
-            "/path/param-django-uuid-str/31ea378c-c052-4b4c-bf0b-679ce5cfcc2a",
+            "/path/param-django-uuid-notype/31ea378c-c052-4b4c-bf0b-679ce5cfcc2a",
+            200,
+            "31ea378c-c052-4b4c-bf0b-679ce5cfcc2a",
+        ),
+        (
+            "/path/param-django-uuid-typestr/31ea378c-c052-4b4c-bf0b-679ce5cfcc2a",
             200,
             "31ea378c-c052-4b4c-bf0b-679ce5cfcc2a",
         ),