Skip to content

Commit 644a617

Browse files
authored
Merge pull request #1463 from vitalik/1205-discriminator
Fix for Annotated Union with Discriminator cannot be used as payload
2 parents 3a02b9e + efe2346 commit 644a617

File tree

2 files changed

+119
-1
lines changed

2 files changed

+119
-1
lines changed

ninja/signature/details.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,15 @@ def _get_param_type(self, name: str, arg: inspect.Parameter) -> FuncParam:
286286

287287
def is_pydantic_model(cls: Any) -> bool:
288288
try:
289-
if get_origin(cls) in UNION_TYPES:
289+
origin = get_origin(cls)
290+
291+
# Handle Annotated types - extract the actual type
292+
if origin is Annotated:
293+
args = get_args(cls)
294+
return is_pydantic_model(args[0])
295+
296+
# Handle Union types
297+
if origin in UNION_TYPES:
290298
return any(issubclass(arg, pydantic.BaseModel) for arg in get_args(cls))
291299
return issubclass(cls, pydantic.BaseModel)
292300
except TypeError: # pragma: no cover

tests/test_discriminator.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from typing import Union
2+
3+
from pydantic import Field
4+
from typing_extensions import Annotated, Literal
5+
6+
from ninja import NinjaAPI, Schema
7+
from ninja.testing import TestClient
8+
9+
10+
class Example1(Schema):
11+
label: Literal["ONE"]
12+
value: float
13+
14+
15+
class Example2(Schema):
16+
label: Literal["TWO"]
17+
value: int
18+
19+
20+
# Annotated union with discriminator
21+
UnionDiscriminator = Annotated[Union[Example1, Example2], Field(discriminator="label")]
22+
23+
# Regular union without annotation
24+
RegularUnion = Union[Example1, Example2]
25+
26+
27+
api = NinjaAPI()
28+
29+
30+
@api.post("/descr-union")
31+
def create_example(request, payload: UnionDiscriminator):
32+
return {"data": payload.model_dump(), "type": payload.__class__.__name__}
33+
34+
35+
@api.post("/regular-union")
36+
def create_example_regular(request, payload: RegularUnion):
37+
return {"data": payload.model_dump(), "type": payload.__class__.__name__}
38+
39+
40+
client = TestClient(api)
41+
42+
43+
def test_schema():
44+
schema = api.get_openapi_schema()
45+
detail1 = schema["paths"]["/api/descr-union"]["post"]["requestBody"]["content"][
46+
"application/json"
47+
]["schema"]
48+
detail2 = schema["paths"]["/api/regular-union"]["post"]["requestBody"]["content"][
49+
"application/json"
50+
]["schema"]
51+
52+
# First method should have 'discriminator' in OpenAPI api
53+
assert "discriminator" in detail1
54+
assert detail1["discriminator"] == {
55+
"mapping": {
56+
"ONE": "#/components/schemas/Example1",
57+
"TWO": "#/components/schemas/Example2",
58+
},
59+
"propertyName": "label",
60+
}
61+
62+
# Second method should NOT have 'discriminator'
63+
assert "discriminator" not in detail2
64+
65+
66+
def test_annotated_union_with_discriminator():
67+
# Test Example1
68+
response = client.post(
69+
"/descr-union",
70+
json={"label": "ONE", "value": "42"},
71+
)
72+
assert response.status_code == 200
73+
assert response.json() == {
74+
"data": {"label": "ONE", "value": 42.0},
75+
"type": "Example1",
76+
}
77+
78+
# Test Example2
79+
response = client.post(
80+
"/descr-union",
81+
json={"label": "TWO", "value": "42"},
82+
)
83+
assert response.status_code == 200
84+
assert response.json() == {
85+
"data": {"label": "TWO", "value": 42},
86+
"type": "Example2",
87+
}
88+
89+
90+
def test_regular_union():
91+
# Test that regular unions still work
92+
response = client.post(
93+
"/regular-union",
94+
json={"label": "ONE", "value": "2025"},
95+
)
96+
assert response.status_code == 200
97+
assert response.json() == {
98+
"data": {"label": "ONE", "value": 2025},
99+
"type": "Example1",
100+
}
101+
102+
response = client.post(
103+
"/regular-union",
104+
json={"label": "TWO", "value": 123},
105+
)
106+
assert response.status_code == 200
107+
assert response.json() == {
108+
"data": {"label": "TWO", "value": 123},
109+
"type": "Example2",
110+
}

0 commit comments

Comments
 (0)