Coverage for tests/test_pydantic_v1_v2_list.py: 100%
137 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-12-04 08:29 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2025-12-04 08:29 +0000
1import sys 1abcdefQ
2from typing import Any, List, Union 1abcdefQ
4from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 1abcdefQ
6if sys.version_info >= (3, 14): 1abcdefQ
7 skip_module_if_py_gte_314() 1Q
9from fastapi import FastAPI 1abcdef
10from fastapi._compat.v1 import BaseModel 1abcdef
11from fastapi.testclient import TestClient 1abcdef
12from inline_snapshot import snapshot 1abcdef
15class SubItem(BaseModel): 1abcdef
16 name: str 1abcdef
19class Item(BaseModel): 1abcdef
20 title: str 1abcdef
21 size: int 1abcdef
22 description: Union[str, None] = None 1abcdef
23 sub: SubItem 1abcdef
24 multi: List[SubItem] = [] 1abcdef
27app = FastAPI() 1abcdef
30@app.post("/item") 1abcdef
31def handle_item(data: Item) -> List[Item]: 1abcdef
32 return [data, data] 1EFGHIJ
35@app.post("/item-filter", response_model=List[Item]) 1abcdef
36def handle_item_filter(data: Item) -> Any: 1abcdef
37 extended_data = data.dict() 1ghijkl
38 extended_data.update({"secret_data": "classified", "internal_id": 12345}) 1ghijkl
39 extended_data["sub"].update({"internal_id": 67890}) 1ghijkl
40 return [extended_data, extended_data] 1ghijkl
43@app.post("/item-list") 1abcdef
44def handle_item_list(data: List[Item]) -> Item: 1abcdef
45 if data: 1RSTUVWXYZ012
46 return data[0] 1RTVXZ1
47 return Item(title="", size=0, sub=SubItem(name="")) 1SUWY02
50@app.post("/item-list-filter", response_model=Item) 1abcdef
51def handle_item_list_filter(data: List[Item]) -> Any: 1abcdef
52 if data: 1s3t4u5v6w7x8
53 extended_data = data[0].dict() 1stuvwx
54 extended_data.update({"secret_data": "classified", "internal_id": 12345}) 1stuvwx
55 extended_data["sub"].update({"internal_id": 67890}) 1stuvwx
56 return extended_data 1stuvwx
57 return Item(title="", size=0, sub=SubItem(name="")) 1345678
60@app.post("/item-list-to-list") 1abcdef
61def handle_item_list_to_list(data: List[Item]) -> List[Item]: 1abcdef
62 return data 1yzABCD
65@app.post("/item-list-to-list-filter", response_model=List[Item]) 1abcdef
66def handle_item_list_to_list_filter(data: List[Item]) -> Any: 1abcdef
67 if data: 1m9n!o#p$q%r'
68 extended_data = data[0].dict() 1mnopqr
69 extended_data.update({"secret_data": "classified", "internal_id": 12345}) 1mnopqr
70 extended_data["sub"].update({"internal_id": 67890}) 1mnopqr
71 return [extended_data, extended_data] 1mnopqr
72 return [] 19!#$%'
75client = TestClient(app) 1abcdef
78def test_item_to_list(): 1abcdef
79 response = client.post( 1EFGHIJ
80 "/item",
81 json={
82 "title": "Test Item",
83 "size": 100,
84 "description": "This is a test item",
85 "sub": {"name": "SubItem1"},
86 "multi": [{"name": "Multi1"}, {"name": "Multi2"}],
87 },
88 )
89 assert response.status_code == 200, response.text 1EFGHIJ
90 result = response.json() 1EFGHIJ
91 assert isinstance(result, list) 1EFGHIJ
92 assert len(result) == 2 1EFGHIJ
93 for item in result: 1EFGHIJ
94 assert item == { 1EFGHIJ
95 "title": "Test Item",
96 "size": 100,
97 "description": "This is a test item",
98 "sub": {"name": "SubItem1"},
99 "multi": [{"name": "Multi1"}, {"name": "Multi2"}],
100 }
103def test_item_to_list_filter(): 1abcdef
104 response = client.post( 1ghijkl
105 "/item-filter",
106 json={
107 "title": "Filtered Item",
108 "size": 200,
109 "description": "Test filtering",
110 "sub": {"name": "SubFiltered"},
111 "multi": [],
112 },
113 )
114 assert response.status_code == 200, response.text 1ghijkl
115 result = response.json() 1ghijkl
116 assert isinstance(result, list) 1ghijkl
117 assert len(result) == 2 1ghijkl
118 for item in result: 1ghijkl
119 assert item == { 1ghijkl
120 "title": "Filtered Item",
121 "size": 200,
122 "description": "Test filtering",
123 "sub": {"name": "SubFiltered"},
124 "multi": [],
125 }
126 # Verify secret fields are filtered out
127 assert "secret_data" not in item 1ghijkl
128 assert "internal_id" not in item 1ghijkl
129 assert "internal_id" not in item["sub"] 1ghijkl
132def test_list_to_item(): 1abcdef
133 response = client.post( 1RTVXZ1
134 "/item-list",
135 json=[
136 {"title": "First Item", "size": 50, "sub": {"name": "First Sub"}},
137 {"title": "Second Item", "size": 75, "sub": {"name": "Second Sub"}},
138 ],
139 )
140 assert response.status_code == 200, response.text 1RTVXZ1
141 assert response.json() == { 1RTVXZ1
142 "title": "First Item",
143 "size": 50,
144 "description": None,
145 "sub": {"name": "First Sub"},
146 "multi": [],
147 }
150def test_list_to_item_empty(): 1abcdef
151 response = client.post( 1SUWY02
152 "/item-list",
153 json=[],
154 )
155 assert response.status_code == 200, response.text 1SUWY02
156 assert response.json() == { 1SUWY02
157 "title": "",
158 "size": 0,
159 "description": None,
160 "sub": {"name": ""},
161 "multi": [],
162 }
165def test_list_to_item_filter(): 1abcdef
166 response = client.post( 1stuvwx
167 "/item-list-filter",
168 json=[
169 {
170 "title": "First Item",
171 "size": 100,
172 "sub": {"name": "First Sub"},
173 "multi": [{"name": "Multi1"}],
174 },
175 {"title": "Second Item", "size": 200, "sub": {"name": "Second Sub"}},
176 ],
177 )
178 assert response.status_code == 200, response.text 1stuvwx
179 result = response.json() 1stuvwx
180 assert result == { 1stuvwx
181 "title": "First Item",
182 "size": 100,
183 "description": None,
184 "sub": {"name": "First Sub"},
185 "multi": [{"name": "Multi1"}],
186 }
187 # Verify secret fields are filtered out
188 assert "secret_data" not in result 1stuvwx
189 assert "internal_id" not in result 1stuvwx
192def test_list_to_item_filter_no_data(): 1abcdef
193 response = client.post("/item-list-filter", json=[]) 1345678
194 assert response.status_code == 200, response.text 1345678
195 assert response.json() == { 1345678
196 "title": "",
197 "size": 0,
198 "description": None,
199 "sub": {"name": ""},
200 "multi": [],
201 }
204def test_list_to_list(): 1abcdef
205 input_items = [ 1yzABCD
206 {"title": "Item 1", "size": 10, "sub": {"name": "Sub1"}},
207 {
208 "title": "Item 2",
209 "size": 20,
210 "description": "Second item",
211 "sub": {"name": "Sub2"},
212 "multi": [{"name": "M1"}, {"name": "M2"}],
213 },
214 {"title": "Item 3", "size": 30, "sub": {"name": "Sub3"}},
215 ]
216 response = client.post( 1yzABCD
217 "/item-list-to-list",
218 json=input_items,
219 )
220 assert response.status_code == 200, response.text 1yzABCD
221 result = response.json() 1yzABCD
222 assert isinstance(result, list) 1yzABCD
223 assert len(result) == 3 1yzABCD
224 assert result[0] == { 1yzABCD
225 "title": "Item 1",
226 "size": 10,
227 "description": None,
228 "sub": {"name": "Sub1"},
229 "multi": [],
230 }
231 assert result[1] == { 1yzABCD
232 "title": "Item 2",
233 "size": 20,
234 "description": "Second item",
235 "sub": {"name": "Sub2"},
236 "multi": [{"name": "M1"}, {"name": "M2"}],
237 }
238 assert result[2] == { 1yzABCD
239 "title": "Item 3",
240 "size": 30,
241 "description": None,
242 "sub": {"name": "Sub3"},
243 "multi": [],
244 }
247def test_list_to_list_filter(): 1abcdef
248 response = client.post( 1mnopqr
249 "/item-list-to-list-filter",
250 json=[{"title": "Item 1", "size": 100, "sub": {"name": "Sub1"}}],
251 )
252 assert response.status_code == 200, response.text 1mnopqr
253 result = response.json() 1mnopqr
254 assert isinstance(result, list) 1mnopqr
255 assert len(result) == 2 1mnopqr
256 for item in result: 1mnopqr
257 assert item == { 1mnopqr
258 "title": "Item 1",
259 "size": 100,
260 "description": None,
261 "sub": {"name": "Sub1"},
262 "multi": [],
263 }
264 # Verify secret fields are filtered out
265 assert "secret_data" not in item 1mnopqr
266 assert "internal_id" not in item 1mnopqr
269def test_list_to_list_filter_no_data(): 1abcdef
270 response = client.post( 19!#$%'
271 "/item-list-to-list-filter",
272 json=[],
273 )
274 assert response.status_code == 200, response.text 19!#$%'
275 assert response.json() == [] 19!#$%'
278def test_list_validation_error(): 1abcdef
279 response = client.post( 1KLMNOP
280 "/item-list",
281 json=[
282 {"title": "Valid Item", "size": 100, "sub": {"name": "Sub1"}},
283 {
284 "title": "Invalid Item"
285 # Missing required fields: size and sub
286 },
287 ],
288 )
289 assert response.status_code == 422, response.text 1KLMNOP
290 error_detail = response.json()["detail"] 1KLMNOP
291 assert len(error_detail) == 2 1KLMNOP
292 assert { 1KLMNOP
293 "loc": ["body", 1, "size"],
294 "msg": "field required",
295 "type": "value_error.missing",
296 } in error_detail
297 assert { 1KLMNOP
298 "loc": ["body", 1, "sub"],
299 "msg": "field required",
300 "type": "value_error.missing",
301 } in error_detail
304def test_list_nested_validation_error(): 1abcdef
305 response = client.post( 1()*+,-
306 "/item-list",
307 json=[
308 {"title": "Item with bad sub", "size": 100, "sub": {"wrong_field": "value"}}
309 ],
310 )
311 assert response.status_code == 422, response.text 1()*+,-
312 assert response.json() == snapshot( 1()*+,-
313 {
314 "detail": [
315 {
316 "loc": ["body", 0, "sub", "name"],
317 "msg": "field required",
318 "type": "value_error.missing",
319 }
320 ]
321 }
322 )
325def test_list_type_validation_error(): 1abcdef
326 response = client.post( 1./:;=?
327 "/item-list",
328 json=[{"title": "Item", "size": "not_a_number", "sub": {"name": "Sub"}}],
329 )
330 assert response.status_code == 422, response.text 1./:;=?
331 assert response.json() == snapshot( 1./:;=?
332 {
333 "detail": [
334 {
335 "loc": ["body", 0, "size"],
336 "msg": "value is not a valid integer",
337 "type": "type_error.integer",
338 }
339 ]
340 }
341 )
344def test_invalid_list_structure(): 1abcdef
345 response = client.post( 1@[]^_`
346 "/item-list",
347 json={"title": "Not a list", "size": 100, "sub": {"name": "Sub"}},
348 )
349 assert response.status_code == 422, response.text 1@[]^_`
350 assert response.json() == snapshot( 1@[]^_`
351 {
352 "detail": [
353 {
354 "loc": ["body"],
355 "msg": "value is not a valid list",
356 "type": "type_error.list",
357 }
358 ]
359 }
360 )
363def test_openapi_schema(): 1abcdef
364 response = client.get("/openapi.json") 2{ | } ~ abbb
365 assert response.status_code == 200, response.text 2{ | } ~ abbb
366 assert response.json() == snapshot( 2{ | } ~ abbb
367 {
368 "openapi": "3.1.0",
369 "info": {"title": "FastAPI", "version": "0.1.0"},
370 "paths": {
371 "/item": {
372 "post": {
373 "summary": "Handle Item",
374 "operationId": "handle_item_item_post",
375 "requestBody": {
376 "content": {
377 "application/json": {
378 "schema": pydantic_snapshot(
379 v2=snapshot(
380 {
381 "allOf": [
382 {
383 "$ref": "#/components/schemas/Item"
384 }
385 ],
386 "title": "Data",
387 }
388 ),
389 v1=snapshot(
390 {"$ref": "#/components/schemas/Item"}
391 ),
392 )
393 }
394 },
395 "required": True,
396 },
397 "responses": {
398 "200": {
399 "description": "Successful Response",
400 "content": {
401 "application/json": {
402 "schema": {
403 "items": {
404 "$ref": "#/components/schemas/Item"
405 },
406 "type": "array",
407 "title": "Response Handle Item Item Post",
408 }
409 }
410 },
411 },
412 "422": {
413 "description": "Validation Error",
414 "content": {
415 "application/json": {
416 "schema": {
417 "$ref": "#/components/schemas/HTTPValidationError"
418 }
419 }
420 },
421 },
422 },
423 }
424 },
425 "/item-filter": {
426 "post": {
427 "summary": "Handle Item Filter",
428 "operationId": "handle_item_filter_item_filter_post",
429 "requestBody": {
430 "content": {
431 "application/json": {
432 "schema": pydantic_snapshot(
433 v2=snapshot(
434 {
435 "allOf": [
436 {
437 "$ref": "#/components/schemas/Item"
438 }
439 ],
440 "title": "Data",
441 }
442 ),
443 v1=snapshot(
444 {"$ref": "#/components/schemas/Item"}
445 ),
446 )
447 }
448 },
449 "required": True,
450 },
451 "responses": {
452 "200": {
453 "description": "Successful Response",
454 "content": {
455 "application/json": {
456 "schema": {
457 "items": {
458 "$ref": "#/components/schemas/Item"
459 },
460 "type": "array",
461 "title": "Response Handle Item Filter Item Filter Post",
462 }
463 }
464 },
465 },
466 "422": {
467 "description": "Validation Error",
468 "content": {
469 "application/json": {
470 "schema": {
471 "$ref": "#/components/schemas/HTTPValidationError"
472 }
473 }
474 },
475 },
476 },
477 }
478 },
479 "/item-list": {
480 "post": {
481 "summary": "Handle Item List",
482 "operationId": "handle_item_list_item_list_post",
483 "requestBody": {
484 "content": {
485 "application/json": {
486 "schema": {
487 "items": {"$ref": "#/components/schemas/Item"},
488 "type": "array",
489 "title": "Data",
490 }
491 }
492 },
493 "required": True,
494 },
495 "responses": {
496 "200": {
497 "description": "Successful Response",
498 "content": {
499 "application/json": {
500 "schema": {"$ref": "#/components/schemas/Item"}
501 }
502 },
503 },
504 "422": {
505 "description": "Validation Error",
506 "content": {
507 "application/json": {
508 "schema": {
509 "$ref": "#/components/schemas/HTTPValidationError"
510 }
511 }
512 },
513 },
514 },
515 }
516 },
517 "/item-list-filter": {
518 "post": {
519 "summary": "Handle Item List Filter",
520 "operationId": "handle_item_list_filter_item_list_filter_post",
521 "requestBody": {
522 "content": {
523 "application/json": {
524 "schema": {
525 "items": {"$ref": "#/components/schemas/Item"},
526 "type": "array",
527 "title": "Data",
528 }
529 }
530 },
531 "required": True,
532 },
533 "responses": {
534 "200": {
535 "description": "Successful Response",
536 "content": {
537 "application/json": {
538 "schema": {"$ref": "#/components/schemas/Item"}
539 }
540 },
541 },
542 "422": {
543 "description": "Validation Error",
544 "content": {
545 "application/json": {
546 "schema": {
547 "$ref": "#/components/schemas/HTTPValidationError"
548 }
549 }
550 },
551 },
552 },
553 }
554 },
555 "/item-list-to-list": {
556 "post": {
557 "summary": "Handle Item List To List",
558 "operationId": "handle_item_list_to_list_item_list_to_list_post",
559 "requestBody": {
560 "content": {
561 "application/json": {
562 "schema": {
563 "items": {"$ref": "#/components/schemas/Item"},
564 "type": "array",
565 "title": "Data",
566 }
567 }
568 },
569 "required": True,
570 },
571 "responses": {
572 "200": {
573 "description": "Successful Response",
574 "content": {
575 "application/json": {
576 "schema": {
577 "items": {
578 "$ref": "#/components/schemas/Item"
579 },
580 "type": "array",
581 "title": "Response Handle Item List To List Item List To List Post",
582 }
583 }
584 },
585 },
586 "422": {
587 "description": "Validation Error",
588 "content": {
589 "application/json": {
590 "schema": {
591 "$ref": "#/components/schemas/HTTPValidationError"
592 }
593 }
594 },
595 },
596 },
597 }
598 },
599 "/item-list-to-list-filter": {
600 "post": {
601 "summary": "Handle Item List To List Filter",
602 "operationId": "handle_item_list_to_list_filter_item_list_to_list_filter_post",
603 "requestBody": {
604 "content": {
605 "application/json": {
606 "schema": {
607 "items": {"$ref": "#/components/schemas/Item"},
608 "type": "array",
609 "title": "Data",
610 }
611 }
612 },
613 "required": True,
614 },
615 "responses": {
616 "200": {
617 "description": "Successful Response",
618 "content": {
619 "application/json": {
620 "schema": {
621 "items": {
622 "$ref": "#/components/schemas/Item"
623 },
624 "type": "array",
625 "title": "Response Handle Item List To List Filter Item List To List Filter Post",
626 }
627 }
628 },
629 },
630 "422": {
631 "description": "Validation Error",
632 "content": {
633 "application/json": {
634 "schema": {
635 "$ref": "#/components/schemas/HTTPValidationError"
636 }
637 }
638 },
639 },
640 },
641 }
642 },
643 },
644 "components": {
645 "schemas": {
646 "HTTPValidationError": {
647 "properties": {
648 "detail": {
649 "items": {
650 "$ref": "#/components/schemas/ValidationError"
651 },
652 "type": "array",
653 "title": "Detail",
654 }
655 },
656 "type": "object",
657 "title": "HTTPValidationError",
658 },
659 "Item": {
660 "properties": {
661 "title": {"type": "string", "title": "Title"},
662 "size": {"type": "integer", "title": "Size"},
663 "description": {"type": "string", "title": "Description"},
664 "sub": {"$ref": "#/components/schemas/SubItem"},
665 "multi": {
666 "items": {"$ref": "#/components/schemas/SubItem"},
667 "type": "array",
668 "title": "Multi",
669 "default": [],
670 },
671 },
672 "type": "object",
673 "required": ["title", "size", "sub"],
674 "title": "Item",
675 },
676 "SubItem": {
677 "properties": {"name": {"type": "string", "title": "Name"}},
678 "type": "object",
679 "required": ["name"],
680 "title": "SubItem",
681 },
682 "ValidationError": {
683 "properties": {
684 "loc": {
685 "items": {
686 "anyOf": [{"type": "string"}, {"type": "integer"}]
687 },
688 "type": "array",
689 "title": "Location",
690 },
691 "msg": {"type": "string", "title": "Message"},
692 "type": {"type": "string", "title": "Error Type"},
693 },
694 "type": "object",
695 "required": ["loc", "msg", "type"],
696 "title": "ValidationError",
697 },
698 }
699 },
700 }
701 )