Repository: vitalik/django-ninja (python)

Issue:
[BUG] Python 3.10: union compatibility (for payload)
**Describe the bug**
The behaviour introduced in #301 to handle `Union`-types, is no longer working on the new style union-type syntax (`|`).

```pycon
>>> from typing import Optional, Union, get_origin
>>> get_origin(Union[int, str])
typing.Union
>>> get_origin(int | str)
<class 'types.UnionType'>
>>> get_origin(Optional[int])
typing.Union
>>> get_origin(int | None)
<class 'types.UnionType'>

```

My proposed solution would be to loosen the test in `is_pydantic_model` to include the new type.
https://github.com/vitalik/django-ninja/blob/432ab5313bef53941441a44bf7882ba9656abd43/ninja/signature/details.py#L248-L254

**Versions (please complete the following information):**
 - Python version: 3.10.9
 - Django version: 4.1.7
 - Django-Ninja version: 0.20.0
 - Pydantic version: 1.10.5




Pull request description:
Support both new and old style Union/Optional
The Python 3.10+ syntax of creating a union or optional type (with `str | int`), results in a different type than using the pre-Python 3.10 syntax (`Union[str, int]`).

```pycon
>>> from typing import Optional, Union, get_origin
>>> get_origin(Union[int, str])
typing.Union
>>> get_origin(int | str)
<class 'types.UnionType'>
>>> get_origin(Optional[int])
typing.Union
>>> get_origin(int | None)
<class 'types.UnionType'>
```

I'm curious about how to update the test so that it works on both Python 3.9 and Python 3.10.
If you can point me in the right direction, I'll gladly update it.

This would fix #685.


Patch:
diff --git a/ninja/signature/details.py b/ninja/signature/details.py
index a740684..534b45e 100644
--- a/ninja/signature/details.py
+++ b/ninja/signature/details.py
@@ -24,6 +24,13 @@ FuncParam = namedtuple(
     "FuncParam", ["name", "alias", "source", "annotation", "is_collection"]
 )
 
+try:
+    from types import UnionType
+
+    UNION_TYPES = (Union, UnionType)
+except ImportError:
+    UNION_TYPES = (Union,)
+
 
 class ViewSignature:
     FLATTEN_PATH_SEP = (
@@ -247,7 +254,7 @@ class ViewSignature:
 
 def is_pydantic_model(cls: Any) -> bool:
     try:
-        if get_collection_origin(cls) == Union:
+        if get_collection_origin(cls) in UNION_TYPES:
             return any(issubclass(arg, pydantic.BaseModel) for arg in get_args(cls))
         return issubclass(cls, pydantic.BaseModel)
     except TypeError:


Test patch:
diff --git a/tests/test_openapi_schema.py b/tests/test_openapi_schema.py
index f0e6ab7..66ca33c 100644
--- a/tests/test_openapi_schema.py
+++ b/tests/test_openapi_schema.py
@@ -1,3 +1,4 @@
+import sys
 from typing import List, Union
 from unittest.mock import Mock
 
@@ -107,6 +108,14 @@ def method_union_payload_and_simple(request, data: Union[int, TypeB]):
     return data.dict()
 
 
+if sys.version_info >= (3, 10):
+    # This requires Python 3.10 or higher (PEP 604), so we're using eval to
+    # conditionally make it available
+    @api.post("/test-new-union-type", response=Response)
+    def method_new_union_payload(request, data: "TypeA | TypeB"):
+        return dict(i=data.i, f=data.f)
+
+
 @api.post(
     "/test-title-description/",
     tags=["a-tag"],
@@ -659,6 +668,29 @@ def test_union_payload_simple(schema):
     }
 
 
+@pytest.mark.skipif(
+    sys.version_info < (3, 10),
+    reason="requires Python 3.10 or higher (PEP 604)",
+)
+def test_new_union_payload_type(schema):
+    method = schema["paths"]["/api/test-new-union-type"]["post"]
+
+    assert method["requestBody"] == {
+        "content": {
+            "application/json": {
+                "schema": {
+                    "anyOf": [
+                        {"$ref": "#/components/schemas/TypeA"},
+                        {"$ref": "#/components/schemas/TypeB"},
+                    ],
+                    "title": "Data",
+                }
+            }
+        },
+        "required": True,
+    }
+
+
 def test_get_openapi_urls():
     api = NinjaAPI(openapi_url=None)
     paths = get_openapi_urls(api)