@@ -149,167 +149,19 @@ def view_multi_model_union(request, data: MultiModelUnion):
149149 return data .model_dump ()
150150
151151
152- # Create models that will cause field name collisions during flattening
153- class ModelA (BaseModel ):
154- name : str
155-
156-
157- class ModelB (BaseModel ):
158- name : str # Same field name as ModelA
159-
160-
161- # Test case to trigger model field collision error
162- try :
163- router_collision = Router ()
164-
165- @router_collision .post ("/test-model-collision" )
166- def view_model_collision (
167- model_a : ModelA ,
168- model_b : ModelB , # Both have 'name' field - should cause collision during flattening
169- ):
170- return {"result" : "collision" }
171-
172- except Exception :
173- # Expected to fail during router creation if collision detection works
174- pass
175-
176-
177- # Test to trigger ConfigError - duplicate name collision in union with Query(None)
178- def test_union_query_name_collision ():
179- """Test that duplicate union parameter names with Query(None) raise ConfigError."""
180-
181- # Create a test that should cause a name collision during flattening
182- try :
183- api = NinjaAPI ()
184- router_test = Router ()
185-
186- # This should trigger the ConfigError when both parameters
187- # have the same alias and are processed as Union[Model, None] = Query(None)
188- @router_test .post ("/collision-test" )
189- def collision_endpoint (
190- # Both parameters have same alias "person" - should cause collision
191- person1 : Union [PersonSchema , None ] = Query (None , alias = "person" ),
192- person2 : Union [PersonSchema , None ] = Query (
193- None , alias = "person"
194- ), # Same alias!
195- ):
196- return {"result" : "should not reach here" }
197-
198- api .add_router ("/test" , router_test )
199-
200- # This should fail during router creation due to name collision
201- assert False , "Expected ConfigError for duplicate name collision" # noqa: B011
202-
203- except ConfigError as e :
204- # This is the expected behavior
205- assert "Duplicated name" in str (e )
206- assert "person" in str (e )
207-
208-
209152@router .post ("/test-union-with-none" )
210153def view_union_with_none (request , optional : Union [str , None ] = Query (None )):
211154 """Test Union[str, None]"""
212155 return {"optional" : optional }
213156
214157
215- # Test union field with multiple pydantic models
216- class UnionFieldTestModel (BaseModel ):
217- choice : Union [SomeModel , OtherModel ]
218-
219-
220- @router .post ("/test-union-field-model" )
221- def view_union_field_model (request , model : UnionFieldTestModel ):
222- """Test union field with multiple pydantic models."""
223- return model .model_dump ()
224-
225-
226- # Test for collection detection with unions
227158class CollectionUnionModel (BaseModel ):
228159 items : List [str ]
229160 nested : Union [SomeModel , None ] = None
230161
231162
232163@router .post ("/test-collection-union" )
233164def view_collection_union (request , data : CollectionUnionModel ):
234- """Test collection fields with union to trigger detect_collection_fields."""
235- return data .model_dump ()
236-
237-
238- # Additional model for testing complex nested union fields
239- class ComplexUnionField (BaseModel ):
240- model_choice : Union [SomeModel , OtherModel ] # No None, no default
241- name : str = "test"
242-
243-
244- @router .post ("/test-complex-union-field" )
245- def view_complex_union_field (request , complex_data : ComplexUnionField ):
246- """Test complex union field processing."""
247- return complex_data .model_dump ()
248-
249-
250- # Model with union field that has NO default
251- class NoDefaultUnionModel (BaseModel ):
252- # This union field has NO default value and contains pydantic models
253- required_union : Union [SomeModel , OtherModel ]
254-
255-
256- @router .post ("/test-no-default-union" )
257- def view_no_default_union (request , no_default : NoDefaultUnionModel ):
258- """Test union field with no default."""
259- return no_default .model_dump ()
260-
261-
262- # Complex nested model to trigger detect_collection_fields union logic
263- class NestedWithCollections (BaseModel ):
264- items : List [str ] # Collection field
265-
266-
267- class DeepModel (BaseModel ):
268- # Nested model that contains a union field
269- nested : Union [NestedWithCollections , SomeModel ]
270- simple_field : str = "test"
271-
272-
273- class VeryDeepModel (BaseModel ):
274- # Multiple levels of nesting to create longer flatten paths
275- deep : DeepModel
276- extra_items : List [int ] = []
277-
278-
279- @router .post ("/test-deep-nested-union" )
280- def view_deep_nested_union (request , deep_data : VeryDeepModel ):
281- """Test deeply nested structure with unions to trigger detect_collection_fields logic."""
282- return deep_data .model_dump ()
283-
284-
285- # trigger _model_flatten_map with Union containing None
286- @router .post ("/test-flatten-union-with-none" )
287- def view_flatten_union_with_none (request , data : Union [SomeModel , None ]):
288- """Test direct Union[Model, None]"""
289- return data .model_dump () if data else {"result" : "none" }
290-
291-
292- # nested union with None
293- class ModelWithUnionField (BaseModel ):
294- union_field : Union [SomeModel , None ] = (
295- None # This should trigger _model_flatten_map with Union
296- )
297-
298-
299- @router .post ("/test-nested-union-with-none" )
300- def view_nested_union_with_none (request , data : ModelWithUnionField ):
301- """Test nested Union[Model, None]"""
302- return data .model_dump ()
303-
304-
305- # Union parameter that gets flattened
306- class OuterModel (BaseModel ):
307- inner : Union [SomeModel , OtherModel , None ] # Union with None at top level
308-
309-
310- @router .post ("/test-direct-union-flattening" )
311- def view_direct_union_flattening (request , data : OuterModel ):
312- """Test direct union flattening."""
313165 return data .model_dump ()
314166
315167
@@ -403,17 +255,6 @@ def view_direct_union_flattening(request, data: OuterModel):
403255 dict (json = None ),
404256 {"optional" : "test" },
405257 ),
406- # Test union field model
407- (
408- "/test-union-field-model" ,
409- dict (json = {"choice" : {"i" : 1 , "s" : "test" , "f" : 1.5 }}),
410- {"choice" : {"i" : 1 , "s" : "test" , "f" : 1.5 , "n" : None }},
411- ),
412- (
413- "/test-union-field-model" ,
414- dict (json = {"choice" : {"x" : 10 , "y" : 20 }}),
415- {"choice" : {"x" : 10 , "y" : 20 }},
416- ),
417258 # Test collection union model
418259 (
419260 "/test-collection-union" ,
@@ -425,110 +266,6 @@ def view_direct_union_flattening(request, data: OuterModel):
425266 dict (json = {"items" : ["x" ], "nested" : {"i" : 5 , "s" : "test" , "f" : 2.0 }}),
426267 {"items" : ["x" ], "nested" : {"i" : 5 , "s" : "test" , "f" : 2.0 , "n" : None }},
427268 ),
428- # Test complex union field
429- (
430- "/test-complex-union-field" ,
431- dict (
432- json = {
433- "model_choice" : {"i" : 1 , "s" : "test" , "f" : 1.5 },
434- "name" : "example" ,
435- }
436- ),
437- {
438- "model_choice" : {"i" : 1 , "s" : "test" , "f" : 1.5 , "n" : None },
439- "name" : "example" ,
440- },
441- ),
442- (
443- "/test-complex-union-field" ,
444- dict (json = {"model_choice" : {"x" : 10 , "y" : 20 }, "name" : "example" }),
445- {"model_choice" : {"x" : 10 , "y" : 20 }, "name" : "example" },
446- ),
447- # Test no default union
448- (
449- "/test-no-default-union" ,
450- dict (json = {"required_union" : {"i" : 2 , "s" : "required" , "f" : 2.5 }}),
451- {"required_union" : {"i" : 2 , "s" : "required" , "f" : 2.5 , "n" : None }},
452- ),
453- (
454- "/test-no-default-union" ,
455- dict (json = {"required_union" : {"x" : 5 , "y" : 10 }}),
456- {"required_union" : {"x" : 5 , "y" : 10 }},
457- ),
458- # Test deeply nested union
459- (
460- "/test-deep-nested-union" ,
461- dict (
462- json = {
463- "deep" : {
464- "nested" : {"items" : ["a" , "b" ]},
465- "simple_field" : "deep_test" ,
466- },
467- "extra_items" : [1 , 2 , 3 ],
468- }
469- ),
470- {
471- "deep" : {"nested" : {"items" : ["a" , "b" ]}, "simple_field" : "deep_test" },
472- "extra_items" : [1 , 2 , 3 ],
473- },
474- ),
475- (
476- "/test-deep-nested-union" ,
477- dict (
478- json = {
479- "deep" : {
480- "nested" : {"i" : 1 , "s" : "nested" , "f" : 1.0 },
481- "simple_field" : "deep_test2" ,
482- },
483- "extra_items" : [],
484- }
485- ),
486- {
487- "deep" : {
488- "nested" : {"i" : 1 , "s" : "nested" , "f" : 1.0 , "n" : None },
489- "simple_field" : "deep_test2" ,
490- },
491- "extra_items" : [],
492- },
493- ),
494- # Test to trigger _model_flatten_map with Union containing None
495- (
496- "/test-flatten-union-with-none" ,
497- dict (json = {"i" : 1 , "s" : "test" , "f" : 1.5 }),
498- {"i" : 1 , "s" : "test" , "f" : 1.5 , "n" : None },
499- ),
500- (
501- "/test-flatten-union-with-none" ,
502- dict (json = None ),
503- {"result" : "none" },
504- ),
505- # Test nested union with None
506- (
507- "/test-nested-union-with-none" ,
508- dict (json = {"union_field" : {"i" : 1 , "s" : "test" , "f" : 1.5 }}),
509- {"union_field" : {"i" : 1 , "s" : "test" , "f" : 1.5 , "n" : None }},
510- ),
511- (
512- "/test-nested-union-with-none" ,
513- dict (json = {"union_field" : None }),
514- {"union_field" : None },
515- ),
516- # Test direct union flattening
517- (
518- "/test-direct-union-flattening" ,
519- dict (json = {"inner" : {"i" : 1 , "s" : "test" , "f" : 1.5 }}),
520- {"inner" : {"i" : 1 , "s" : "test" , "f" : 1.5 , "n" : None }},
521- ),
522- (
523- "/test-direct-union-flattening" ,
524- dict (json = {"inner" : {"x" : 10 , "y" : 20 }}),
525- {"inner" : {"x" : 10 , "y" : 20 }},
526- ),
527- (
528- "/test-direct-union-flattening" ,
529- dict (json = {"inner" : None }),
530- {"inner" : None },
531- ),
532269 (
533270 "/test-multi-model-union" ,
534271 dict (json = {"models" : {"i" : 1 , "s" : "test" , "f" : 1.5 }}),
@@ -564,3 +301,33 @@ def test_invalid_body():
564301 assert response .json () == {
565302 "detail" : "Cannot parse request body" ,
566303 }
304+
305+
306+ def test_union_query_name_collision ():
307+ """Test that duplicate union parameter names with Query(None) raise ConfigError."""
308+
309+ with pytest .raises (ConfigError , match = r"Duplicated name.*person" ):
310+ api = NinjaAPI ()
311+ router_test = Router ()
312+
313+ @router_test .post ("/collision-test" )
314+ def collision_endpoint (
315+ person1 : Union [PersonSchema , None ] = Query (None , alias = "person" ),
316+ person2 : Union [PersonSchema , None ] = Query (None , alias = "person" ),
317+ ):
318+ return {"result" : "should not reach here" }
319+
320+ api .add_router ("/test" , router_test )
321+
322+
323+ def test_union_with_none_body_param ():
324+ """Test Union[Model, None] parameter"""
325+
326+ test_router = Router ()
327+
328+ @test_router .post ("/test-union-none-body" )
329+ def test_union_none_body (request , data : Union [SomeModel , None ]):
330+ return data .model_dump () if data else {"result" : "none" }
331+
332+ # Verify the router was created successfully and has one registered operation
333+ assert len (test_router .path_operations ) == 1
0 commit comments