Coverage for tests/test_pydantic_v1_v2_mixed.py: 100%
206 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 1abcdef8
2from typing import Any, List, Union 1abcdef8
4from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 1abcdef8
6if sys.version_info >= (3, 14): 1abcdef8
7 skip_module_if_py_gte_314() 18
9from fastapi import FastAPI 1abcdef
10from fastapi._compat.v1 import BaseModel 1abcdef
11from fastapi.testclient import TestClient 1abcdef
12from inline_snapshot import snapshot 1abcdef
13from pydantic import BaseModel as NewBaseModel 1abcdef
16class SubItem(BaseModel): 1abcdef
17 name: str 1abcdef
20class Item(BaseModel): 1abcdef
21 title: str 1abcdef
22 size: int 1abcdef
23 description: Union[str, None] = None 1abcdef
24 sub: SubItem 1abcdef
25 multi: List[SubItem] = [] 1abcdef
28class NewSubItem(NewBaseModel): 1abcdef
29 new_sub_name: str 1abcdef
32class NewItem(NewBaseModel): 1abcdef
33 new_title: str 1abcdef
34 new_size: int 1abcdef
35 new_description: Union[str, None] = None 1abcdef
36 new_sub: NewSubItem 1abcdef
37 new_multi: List[NewSubItem] = [] 1abcdef
40app = FastAPI() 1abcdef
43@app.post("/v1-to-v2/item") 1abcdef
44def handle_v1_item_to_v2(data: Item) -> NewItem: 1abcdef
45 return NewItem( 2@ [ ] ^ _ ` { | } ~ abbb
46 new_title=data.title,
47 new_size=data.size,
48 new_description=data.description,
49 new_sub=NewSubItem(new_sub_name=data.sub.name),
50 new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi],
51 )
54@app.post("/v1-to-v2/item-filter", response_model=NewItem) 1abcdef
55def handle_v1_item_to_v2_filter(data: Item) -> Any: 1abcdef
56 result = { 1stuvwx
57 "new_title": data.title,
58 "new_size": data.size,
59 "new_description": data.description,
60 "new_sub": {"new_sub_name": data.sub.name, "new_sub_secret": "sub_hidden"},
61 "new_multi": [
62 {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} for s in data.multi
63 ],
64 "secret": "hidden_v1_to_v2",
65 }
66 return result 1stuvwx
69@app.post("/v2-to-v1/item") 1abcdef
70def handle_v2_item_to_v1(data: NewItem) -> Item: 1abcdef
71 return Item( 2cbdbebfbgbhbibjbkblbmbnb
72 title=data.new_title,
73 size=data.new_size,
74 description=data.new_description,
75 sub=SubItem(name=data.new_sub.new_sub_name),
76 multi=[SubItem(name=s.new_sub_name) for s in data.new_multi],
77 )
80@app.post("/v2-to-v1/item-filter", response_model=Item) 1abcdef
81def handle_v2_item_to_v1_filter(data: NewItem) -> Any: 1abcdef
82 result = { 1yzABCD
83 "title": data.new_title,
84 "size": data.new_size,
85 "description": data.new_description,
86 "sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"},
87 "multi": [
88 {"name": s.new_sub_name, "sub_secret": "sub_hidden"} for s in data.new_multi
89 ],
90 "secret": "hidden_v2_to_v1",
91 }
92 return result 1yzABCD
95@app.post("/v1-to-v2/item-to-list") 1abcdef
96def handle_v1_item_to_v2_list(data: Item) -> List[NewItem]: 1abcdef
97 converted = NewItem( 1QRSTUV
98 new_title=data.title,
99 new_size=data.size,
100 new_description=data.description,
101 new_sub=NewSubItem(new_sub_name=data.sub.name),
102 new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi],
103 )
104 return [converted, converted] 1QRSTUV
107@app.post("/v1-to-v2/list-to-list") 1abcdef
108def handle_v1_list_to_v2_list(data: List[Item]) -> List[NewItem]: 1abcdef
109 result = [] 1EFGHIJ
110 for item in data: 1EFGHIJ
111 result.append( 1EFGHIJ
112 NewItem(
113 new_title=item.title,
114 new_size=item.size,
115 new_description=item.description,
116 new_sub=NewSubItem(new_sub_name=item.sub.name),
117 new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi],
118 )
119 )
120 return result 1EFGHIJ
123@app.post("/v1-to-v2/list-to-list-filter", response_model=List[NewItem]) 1abcdef
124def handle_v1_list_to_v2_list_filter(data: List[Item]) -> Any: 1abcdef
125 result = [] 1ghijkl
126 for item in data: 1ghijkl
127 converted = { 1ghijkl
128 "new_title": item.title,
129 "new_size": item.size,
130 "new_description": item.description,
131 "new_sub": {"new_sub_name": item.sub.name, "new_sub_secret": "sub_hidden"},
132 "new_multi": [
133 {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"}
134 for s in item.multi
135 ],
136 "secret": "hidden_v2_to_v1",
137 }
138 result.append(converted) 1ghijkl
139 return result 1ghijkl
142@app.post("/v1-to-v2/list-to-item") 1abcdef
143def handle_v1_list_to_v2_item(data: List[Item]) -> NewItem: 1abcdef
144 if data: 1W9X!Y#Z$0%1'
145 item = data[0] 1WXYZ01
146 return NewItem( 1WXYZ01
147 new_title=item.title,
148 new_size=item.size,
149 new_description=item.description,
150 new_sub=NewSubItem(new_sub_name=item.sub.name),
151 new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi],
152 )
153 return NewItem(new_title="", new_size=0, new_sub=NewSubItem(new_sub_name="")) 19!#$%'
156@app.post("/v2-to-v1/item-to-list") 1abcdef
157def handle_v2_item_to_v1_list(data: NewItem) -> List[Item]: 1abcdef
158 converted = Item( 1()*+,-
159 title=data.new_title,
160 size=data.new_size,
161 description=data.new_description,
162 sub=SubItem(name=data.new_sub.new_sub_name),
163 multi=[SubItem(name=s.new_sub_name) for s in data.new_multi],
164 )
165 return [converted, converted] 1()*+,-
168@app.post("/v2-to-v1/list-to-list") 1abcdef
169def handle_v2_list_to_v1_list(data: List[NewItem]) -> List[Item]: 1abcdef
170 result = [] 1KLMNOP
171 for item in data: 1KLMNOP
172 result.append( 1KLMNOP
173 Item(
174 title=item.new_title,
175 size=item.new_size,
176 description=item.new_description,
177 sub=SubItem(name=item.new_sub.new_sub_name),
178 multi=[SubItem(name=s.new_sub_name) for s in item.new_multi],
179 )
180 )
181 return result 1KLMNOP
184@app.post("/v2-to-v1/list-to-list-filter", response_model=List[Item]) 1abcdef
185def handle_v2_list_to_v1_list_filter(data: List[NewItem]) -> Any: 1abcdef
186 result = [] 1mnopqr
187 for item in data: 1mnopqr
188 converted = { 1mnopqr
189 "title": item.new_title,
190 "size": item.new_size,
191 "description": item.new_description,
192 "sub": {"name": item.new_sub.new_sub_name, "sub_secret": "sub_hidden"},
193 "multi": [
194 {"name": s.new_sub_name, "sub_secret": "sub_hidden"}
195 for s in item.new_multi
196 ],
197 "secret": "hidden_v2_to_v1",
198 }
199 result.append(converted) 1mnopqr
200 return result 1mnopqr
203@app.post("/v2-to-v1/list-to-item") 1abcdef
204def handle_v2_list_to_v1_item(data: List[NewItem]) -> Item: 1abcdef
205 if data: 12.3/4:5;6=7?
206 item = data[0] 1234567
207 return Item( 1234567
208 title=item.new_title,
209 size=item.new_size,
210 description=item.new_description,
211 sub=SubItem(name=item.new_sub.new_sub_name),
212 multi=[SubItem(name=s.new_sub_name) for s in item.new_multi],
213 )
214 return Item(title="", size=0, sub=SubItem(name="")) 1./:;=?
217client = TestClient(app) 1abcdef
220def test_v1_to_v2_item(): 1abcdef
221 response = client.post( 2@ ] _ { } ab
222 "/v1-to-v2/item",
223 json={
224 "title": "Old Item",
225 "size": 100,
226 "description": "V1 description",
227 "sub": {"name": "V1 Sub"},
228 "multi": [{"name": "M1"}, {"name": "M2"}],
229 },
230 )
231 assert response.status_code == 200, response.text 2@ ] _ { } ab
232 assert response.json() == { 2@ ] _ { } ab
233 "new_title": "Old Item",
234 "new_size": 100,
235 "new_description": "V1 description",
236 "new_sub": {"new_sub_name": "V1 Sub"},
237 "new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}],
238 }
241def test_v1_to_v2_item_minimal(): 1abcdef
242 response = client.post( 2[ ^ ` | ~ bb
243 "/v1-to-v2/item",
244 json={"title": "Minimal", "size": 50, "sub": {"name": "MinSub"}},
245 )
246 assert response.status_code == 200, response.text 2[ ^ ` | ~ bb
247 assert response.json() == { 2[ ^ ` | ~ bb
248 "new_title": "Minimal",
249 "new_size": 50,
250 "new_description": None,
251 "new_sub": {"new_sub_name": "MinSub"},
252 "new_multi": [],
253 }
256def test_v1_to_v2_item_filter(): 1abcdef
257 response = client.post( 1stuvwx
258 "/v1-to-v2/item-filter",
259 json={
260 "title": "Filtered Item",
261 "size": 50,
262 "sub": {"name": "Sub"},
263 "multi": [{"name": "Multi1"}],
264 },
265 )
266 assert response.status_code == 200, response.text 1stuvwx
267 result = response.json() 1stuvwx
268 assert result == snapshot( 1stuvwx
269 {
270 "new_title": "Filtered Item",
271 "new_size": 50,
272 "new_description": None,
273 "new_sub": {"new_sub_name": "Sub"},
274 "new_multi": [{"new_sub_name": "Multi1"}],
275 }
276 )
277 # Verify secret fields are filtered out
278 assert "secret" not in result 1stuvwx
279 assert "new_sub_secret" not in result["new_sub"] 1stuvwx
280 assert "new_sub_secret" not in result["new_multi"][0] 1stuvwx
283def test_v2_to_v1_item(): 1abcdef
284 response = client.post( 2cbebgbibkbmb
285 "/v2-to-v1/item",
286 json={
287 "new_title": "New Item",
288 "new_size": 200,
289 "new_description": "V2 description",
290 "new_sub": {"new_sub_name": "V2 Sub"},
291 "new_multi": [{"new_sub_name": "N1"}, {"new_sub_name": "N2"}],
292 },
293 )
294 assert response.status_code == 200, response.text 2cbebgbibkbmb
295 assert response.json() == { 2cbebgbibkbmb
296 "title": "New Item",
297 "size": 200,
298 "description": "V2 description",
299 "sub": {"name": "V2 Sub"},
300 "multi": [{"name": "N1"}, {"name": "N2"}],
301 }
304def test_v2_to_v1_item_minimal(): 1abcdef
305 response = client.post( 2dbfbhbjblbnb
306 "/v2-to-v1/item",
307 json={
308 "new_title": "MinimalNew",
309 "new_size": 75,
310 "new_sub": {"new_sub_name": "MinNewSub"},
311 },
312 )
313 assert response.status_code == 200, response.text 2dbfbhbjblbnb
314 assert response.json() == { 2dbfbhbjblbnb
315 "title": "MinimalNew",
316 "size": 75,
317 "description": None,
318 "sub": {"name": "MinNewSub"},
319 "multi": [],
320 }
323def test_v2_to_v1_item_filter(): 1abcdef
324 response = client.post( 1yzABCD
325 "/v2-to-v1/item-filter",
326 json={
327 "new_title": "Filtered New",
328 "new_size": 75,
329 "new_sub": {"new_sub_name": "NewSub"},
330 "new_multi": [],
331 },
332 )
333 assert response.status_code == 200, response.text 1yzABCD
334 result = response.json() 1yzABCD
335 assert result == snapshot( 1yzABCD
336 {
337 "title": "Filtered New",
338 "size": 75,
339 "description": None,
340 "sub": {"name": "NewSub"},
341 "multi": [],
342 }
343 )
344 # Verify secret fields are filtered out
345 assert "secret" not in result 1yzABCD
346 assert "sub_secret" not in result["sub"] 1yzABCD
349def test_v1_item_to_v2_list(): 1abcdef
350 response = client.post( 1QRSTUV
351 "/v1-to-v2/item-to-list",
352 json={
353 "title": "Single to List",
354 "size": 150,
355 "description": "Convert to list",
356 "sub": {"name": "Sub1"},
357 "multi": [],
358 },
359 )
360 assert response.status_code == 200, response.text 1QRSTUV
361 result = response.json() 1QRSTUV
362 assert result == [ 1QRSTUV
363 {
364 "new_title": "Single to List",
365 "new_size": 150,
366 "new_description": "Convert to list",
367 "new_sub": {"new_sub_name": "Sub1"},
368 "new_multi": [],
369 },
370 {
371 "new_title": "Single to List",
372 "new_size": 150,
373 "new_description": "Convert to list",
374 "new_sub": {"new_sub_name": "Sub1"},
375 "new_multi": [],
376 },
377 ]
380def test_v1_list_to_v2_list(): 1abcdef
381 response = client.post( 1EFGHIJ
382 "/v1-to-v2/list-to-list",
383 json=[
384 {"title": "Item1", "size": 10, "sub": {"name": "Sub1"}},
385 {
386 "title": "Item2",
387 "size": 20,
388 "description": "Second item",
389 "sub": {"name": "Sub2"},
390 "multi": [{"name": "M1"}, {"name": "M2"}],
391 },
392 {"title": "Item3", "size": 30, "sub": {"name": "Sub3"}},
393 ],
394 )
395 assert response.status_code == 200, response.text 1EFGHIJ
396 assert response.json() == [ 1EFGHIJ
397 {
398 "new_title": "Item1",
399 "new_size": 10,
400 "new_description": None,
401 "new_sub": {"new_sub_name": "Sub1"},
402 "new_multi": [],
403 },
404 {
405 "new_title": "Item2",
406 "new_size": 20,
407 "new_description": "Second item",
408 "new_sub": {"new_sub_name": "Sub2"},
409 "new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}],
410 },
411 {
412 "new_title": "Item3",
413 "new_size": 30,
414 "new_description": None,
415 "new_sub": {"new_sub_name": "Sub3"},
416 "new_multi": [],
417 },
418 ]
421def test_v1_list_to_v2_list_filter(): 1abcdef
422 response = client.post( 1ghijkl
423 "/v1-to-v2/list-to-list-filter",
424 json=[{"title": "FilterMe", "size": 30, "sub": {"name": "SubF"}}],
425 )
426 assert response.status_code == 200, response.text 1ghijkl
427 result = response.json() 1ghijkl
428 assert result == snapshot( 1ghijkl
429 [
430 {
431 "new_title": "FilterMe",
432 "new_size": 30,
433 "new_description": None,
434 "new_sub": {"new_sub_name": "SubF"},
435 "new_multi": [],
436 }
437 ]
438 )
439 # Verify secret fields are filtered out
440 assert "secret" not in result[0] 1ghijkl
441 assert "new_sub_secret" not in result[0]["new_sub"] 1ghijkl
444def test_v1_list_to_v2_item(): 1abcdef
445 response = client.post( 1WXYZ01
446 "/v1-to-v2/list-to-item",
447 json=[
448 {"title": "First", "size": 100, "sub": {"name": "FirstSub"}},
449 {"title": "Second", "size": 200, "sub": {"name": "SecondSub"}},
450 ],
451 )
452 assert response.status_code == 200, response.text 1WXYZ01
453 assert response.json() == { 1WXYZ01
454 "new_title": "First",
455 "new_size": 100,
456 "new_description": None,
457 "new_sub": {"new_sub_name": "FirstSub"},
458 "new_multi": [],
459 }
462def test_v1_list_to_v2_item_empty(): 1abcdef
463 response = client.post("/v1-to-v2/list-to-item", json=[]) 19!#$%'
464 assert response.status_code == 200, response.text 19!#$%'
465 assert response.json() == { 19!#$%'
466 "new_title": "",
467 "new_size": 0,
468 "new_description": None,
469 "new_sub": {"new_sub_name": ""},
470 "new_multi": [],
471 }
474def test_v2_item_to_v1_list(): 1abcdef
475 response = client.post( 1()*+,-
476 "/v2-to-v1/item-to-list",
477 json={
478 "new_title": "Single New",
479 "new_size": 250,
480 "new_description": "New to list",
481 "new_sub": {"new_sub_name": "NewSub"},
482 "new_multi": [],
483 },
484 )
485 assert response.status_code == 200, response.text 1()*+,-
486 assert response.json() == [ 1()*+,-
487 {
488 "title": "Single New",
489 "size": 250,
490 "description": "New to list",
491 "sub": {"name": "NewSub"},
492 "multi": [],
493 },
494 {
495 "title": "Single New",
496 "size": 250,
497 "description": "New to list",
498 "sub": {"name": "NewSub"},
499 "multi": [],
500 },
501 ]
504def test_v2_list_to_v1_list(): 1abcdef
505 response = client.post( 1KLMNOP
506 "/v2-to-v1/list-to-list",
507 json=[
508 {"new_title": "New1", "new_size": 15, "new_sub": {"new_sub_name": "NS1"}},
509 {
510 "new_title": "New2",
511 "new_size": 25,
512 "new_description": "Second new",
513 "new_sub": {"new_sub_name": "NS2"},
514 "new_multi": [{"new_sub_name": "NM1"}],
515 },
516 ],
517 )
518 assert response.status_code == 200, response.text 1KLMNOP
519 assert response.json() == [ 1KLMNOP
520 {
521 "title": "New1",
522 "size": 15,
523 "description": None,
524 "sub": {"name": "NS1"},
525 "multi": [],
526 },
527 {
528 "title": "New2",
529 "size": 25,
530 "description": "Second new",
531 "sub": {"name": "NS2"},
532 "multi": [{"name": "NM1"}],
533 },
534 ]
537def test_v2_list_to_v1_list_filter(): 1abcdef
538 response = client.post( 1mnopqr
539 "/v2-to-v1/list-to-list-filter",
540 json=[
541 {
542 "new_title": "FilterNew",
543 "new_size": 35,
544 "new_sub": {"new_sub_name": "NSF"},
545 }
546 ],
547 )
548 assert response.status_code == 200, response.text 1mnopqr
549 result = response.json() 1mnopqr
550 assert result == snapshot( 1mnopqr
551 [
552 {
553 "title": "FilterNew",
554 "size": 35,
555 "description": None,
556 "sub": {"name": "NSF"},
557 "multi": [],
558 }
559 ]
560 )
561 # Verify secret fields are filtered out
562 assert "secret" not in result[0] 1mnopqr
563 assert "sub_secret" not in result[0]["sub"] 1mnopqr
566def test_v2_list_to_v1_item(): 1abcdef
567 response = client.post( 1234567
568 "/v2-to-v1/list-to-item",
569 json=[
570 {
571 "new_title": "FirstNew",
572 "new_size": 300,
573 "new_sub": {"new_sub_name": "FNS"},
574 },
575 {
576 "new_title": "SecondNew",
577 "new_size": 400,
578 "new_sub": {"new_sub_name": "SNS"},
579 },
580 ],
581 )
582 assert response.status_code == 200, response.text 1234567
583 assert response.json() == { 1234567
584 "title": "FirstNew",
585 "size": 300,
586 "description": None,
587 "sub": {"name": "FNS"},
588 "multi": [],
589 }
592def test_v2_list_to_v1_item_empty(): 1abcdef
593 response = client.post("/v2-to-v1/list-to-item", json=[]) 1./:;=?
594 assert response.status_code == 200, response.text 1./:;=?
595 assert response.json() == { 1./:;=?
596 "title": "",
597 "size": 0,
598 "description": None,
599 "sub": {"name": ""},
600 "multi": [],
601 }
604def test_v1_to_v2_validation_error(): 1abcdef
605 response = client.post("/v1-to-v2/item", json={"title": "Missing fields"}) 2obpbqbrbsbtb
606 assert response.status_code == 422, response.text 2obpbqbrbsbtb
607 assert response.json() == snapshot( 2obpbqbrbsbtb
608 {
609 "detail": [
610 {
611 "loc": ["body", "size"],
612 "msg": "field required",
613 "type": "value_error.missing",
614 },
615 {
616 "loc": ["body", "sub"],
617 "msg": "field required",
618 "type": "value_error.missing",
619 },
620 ]
621 }
622 )
625def test_v1_to_v2_nested_validation_error(): 1abcdef
626 response = client.post( 2ubvbwbxbybzb
627 "/v1-to-v2/item",
628 json={"title": "Bad sub", "size": 100, "sub": {"wrong_field": "value"}},
629 )
630 assert response.status_code == 422, response.text 2ubvbwbxbybzb
631 assert response.json() == snapshot( 2ubvbwbxbybzb
632 {
633 "detail": [
634 {
635 "loc": ["body", "sub", "name"],
636 "msg": "field required",
637 "type": "value_error.missing",
638 }
639 ]
640 }
641 )
644def test_v1_to_v2_type_validation_error(): 1abcdef
645 response = client.post( 2AbBbCbDbEbFb
646 "/v1-to-v2/item",
647 json={"title": "Bad type", "size": "not_a_number", "sub": {"name": "Sub"}},
648 )
649 assert response.status_code == 422, response.text 2AbBbCbDbEbFb
650 assert response.json() == snapshot( 2AbBbCbDbEbFb
651 {
652 "detail": [
653 {
654 "loc": ["body", "size"],
655 "msg": "value is not a valid integer",
656 "type": "type_error.integer",
657 }
658 ]
659 }
660 )
663def test_v2_to_v1_validation_error(): 1abcdef
664 response = client.post( 2GbHbIbJbKbLb
665 "/v2-to-v1/item",
666 json={"new_title": "Missing fields"},
667 )
668 assert response.status_code == 422, response.text 2GbHbIbJbKbLb
669 assert response.json() == snapshot( 2GbHbIbJbKbLb
670 {
671 "detail": pydantic_snapshot(
672 v2=snapshot(
673 [
674 {
675 "type": "missing",
676 "loc": ["body", "new_size"],
677 "msg": "Field required",
678 "input": {"new_title": "Missing fields"},
679 },
680 {
681 "type": "missing",
682 "loc": ["body", "new_sub"],
683 "msg": "Field required",
684 "input": {"new_title": "Missing fields"},
685 },
686 ]
687 ),
688 v1=snapshot(
689 [
690 {
691 "loc": ["body", "new_size"],
692 "msg": "field required",
693 "type": "value_error.missing",
694 },
695 {
696 "loc": ["body", "new_sub"],
697 "msg": "field required",
698 "type": "value_error.missing",
699 },
700 ]
701 ),
702 )
703 }
704 )
707def test_v2_to_v1_nested_validation_error(): 1abcdef
708 response = client.post( 2MbNbObPbQbRb
709 "/v2-to-v1/item",
710 json={
711 "new_title": "Bad sub",
712 "new_size": 200,
713 "new_sub": {"wrong_field": "value"},
714 },
715 )
716 assert response.status_code == 422, response.text 2MbNbObPbQbRb
717 assert response.json() == snapshot( 2MbNbObPbQbRb
718 {
719 "detail": [
720 pydantic_snapshot(
721 v2=snapshot(
722 {
723 "type": "missing",
724 "loc": ["body", "new_sub", "new_sub_name"],
725 "msg": "Field required",
726 "input": {"wrong_field": "value"},
727 }
728 ),
729 v1=snapshot(
730 {
731 "loc": ["body", "new_sub", "new_sub_name"],
732 "msg": "field required",
733 "type": "value_error.missing",
734 }
735 ),
736 )
737 ]
738 }
739 )
742def test_v1_list_validation_error(): 1abcdef
743 response = client.post( 2SbTbUbVbWbXb
744 "/v1-to-v2/list-to-list",
745 json=[
746 {"title": "Valid", "size": 10, "sub": {"name": "S"}},
747 {"title": "Invalid"},
748 ],
749 )
750 assert response.status_code == 422, response.text 2SbTbUbVbWbXb
751 assert response.json() == snapshot( 2SbTbUbVbWbXb
752 {
753 "detail": [
754 {
755 "loc": ["body", 1, "size"],
756 "msg": "field required",
757 "type": "value_error.missing",
758 },
759 {
760 "loc": ["body", 1, "sub"],
761 "msg": "field required",
762 "type": "value_error.missing",
763 },
764 ]
765 }
766 )
769def test_v2_list_validation_error(): 1abcdef
770 response = client.post( 2YbZb0b1b2b3b
771 "/v2-to-v1/list-to-list",
772 json=[
773 {"new_title": "Valid", "new_size": 10, "new_sub": {"new_sub_name": "NS"}},
774 {"new_title": "Invalid"},
775 ],
776 )
777 assert response.status_code == 422, response.text 2YbZb0b1b2b3b
778 assert response.json() == snapshot( 2YbZb0b1b2b3b
779 {
780 "detail": pydantic_snapshot(
781 v2=snapshot(
782 [
783 {
784 "type": "missing",
785 "loc": ["body", 1, "new_size"],
786 "msg": "Field required",
787 "input": {"new_title": "Invalid"},
788 },
789 {
790 "type": "missing",
791 "loc": ["body", 1, "new_sub"],
792 "msg": "Field required",
793 "input": {"new_title": "Invalid"},
794 },
795 ]
796 ),
797 v1=snapshot(
798 [
799 {
800 "loc": ["body", 1, "new_size"],
801 "msg": "field required",
802 "type": "value_error.missing",
803 },
804 {
805 "loc": ["body", 1, "new_sub"],
806 "msg": "field required",
807 "type": "value_error.missing",
808 },
809 ]
810 ),
811 )
812 }
813 )
816def test_invalid_list_structure_v1(): 1abcdef
817 response = client.post( 24b5b6b7b8b9b
818 "/v1-to-v2/list-to-list",
819 json={"title": "Not a list", "size": 100, "sub": {"name": "Sub"}},
820 )
821 assert response.status_code == 422, response.text 24b5b6b7b8b9b
822 assert response.json() == snapshot( 24b5b6b7b8b9b
823 {
824 "detail": [
825 {
826 "loc": ["body"],
827 "msg": "value is not a valid list",
828 "type": "type_error.list",
829 }
830 ]
831 }
832 )
835def test_invalid_list_structure_v2(): 1abcdef
836 response = client.post( 2!b#b$b%b'b(b
837 "/v2-to-v1/list-to-list",
838 json={
839 "new_title": "Not a list",
840 "new_size": 100,
841 "new_sub": {"new_sub_name": "Sub"},
842 },
843 )
844 assert response.status_code == 422, response.text 2!b#b$b%b'b(b
845 assert response.json() == snapshot( 2!b#b$b%b'b(b
846 {
847 "detail": pydantic_snapshot(
848 v2=snapshot(
849 [
850 {
851 "type": "list_type",
852 "loc": ["body"],
853 "msg": "Input should be a valid list",
854 "input": {
855 "new_title": "Not a list",
856 "new_size": 100,
857 "new_sub": {"new_sub_name": "Sub"},
858 },
859 }
860 ]
861 ),
862 v1=snapshot(
863 [
864 {
865 "loc": ["body"],
866 "msg": "value is not a valid list",
867 "type": "type_error.list",
868 }
869 ]
870 ),
871 )
872 }
873 )
876def test_openapi_schema(): 1abcdef
877 response = client.get("/openapi.json") 2)b*b+b,b-b.b
878 assert response.status_code == 200, response.text 2)b*b+b,b-b.b
879 assert response.json() == snapshot( 2)b*b+b,b-b.b
880 {
881 "openapi": "3.1.0",
882 "info": {"title": "FastAPI", "version": "0.1.0"},
883 "paths": {
884 "/v1-to-v2/item": {
885 "post": {
886 "summary": "Handle V1 Item To V2",
887 "operationId": "handle_v1_item_to_v2_v1_to_v2_item_post",
888 "requestBody": {
889 "content": {
890 "application/json": {
891 "schema": pydantic_snapshot(
892 v2=snapshot(
893 {
894 "allOf": [
895 {
896 "$ref": "#/components/schemas/Item"
897 }
898 ],
899 "title": "Data",
900 }
901 ),
902 v1=snapshot(
903 {"$ref": "#/components/schemas/Item"}
904 ),
905 )
906 }
907 },
908 "required": True,
909 },
910 "responses": {
911 "200": {
912 "description": "Successful Response",
913 "content": {
914 "application/json": {
915 "schema": {
916 "$ref": "#/components/schemas/NewItem"
917 }
918 }
919 },
920 },
921 "422": {
922 "description": "Validation Error",
923 "content": {
924 "application/json": {
925 "schema": {
926 "$ref": "#/components/schemas/HTTPValidationError"
927 }
928 }
929 },
930 },
931 },
932 }
933 },
934 "/v1-to-v2/item-filter": {
935 "post": {
936 "summary": "Handle V1 Item To V2 Filter",
937 "operationId": "handle_v1_item_to_v2_filter_v1_to_v2_item_filter_post",
938 "requestBody": {
939 "content": {
940 "application/json": {
941 "schema": pydantic_snapshot(
942 v2=snapshot(
943 {
944 "allOf": [
945 {
946 "$ref": "#/components/schemas/Item"
947 }
948 ],
949 "title": "Data",
950 }
951 ),
952 v1=snapshot(
953 {"$ref": "#/components/schemas/Item"}
954 ),
955 )
956 }
957 },
958 "required": True,
959 },
960 "responses": {
961 "200": {
962 "description": "Successful Response",
963 "content": {
964 "application/json": {
965 "schema": {
966 "$ref": "#/components/schemas/NewItem"
967 }
968 }
969 },
970 },
971 "422": {
972 "description": "Validation Error",
973 "content": {
974 "application/json": {
975 "schema": {
976 "$ref": "#/components/schemas/HTTPValidationError"
977 }
978 }
979 },
980 },
981 },
982 }
983 },
984 "/v2-to-v1/item": {
985 "post": {
986 "summary": "Handle V2 Item To V1",
987 "operationId": "handle_v2_item_to_v1_v2_to_v1_item_post",
988 "requestBody": {
989 "content": {
990 "application/json": {
991 "schema": {"$ref": "#/components/schemas/NewItem"}
992 }
993 },
994 "required": True,
995 },
996 "responses": {
997 "200": {
998 "description": "Successful Response",
999 "content": {
1000 "application/json": {
1001 "schema": {"$ref": "#/components/schemas/Item"}
1002 }
1003 },
1004 },
1005 "422": {
1006 "description": "Validation Error",
1007 "content": {
1008 "application/json": {
1009 "schema": {
1010 "$ref": "#/components/schemas/HTTPValidationError"
1011 }
1012 }
1013 },
1014 },
1015 },
1016 }
1017 },
1018 "/v2-to-v1/item-filter": {
1019 "post": {
1020 "summary": "Handle V2 Item To V1 Filter",
1021 "operationId": "handle_v2_item_to_v1_filter_v2_to_v1_item_filter_post",
1022 "requestBody": {
1023 "content": {
1024 "application/json": {
1025 "schema": {"$ref": "#/components/schemas/NewItem"}
1026 }
1027 },
1028 "required": True,
1029 },
1030 "responses": {
1031 "200": {
1032 "description": "Successful Response",
1033 "content": {
1034 "application/json": {
1035 "schema": {"$ref": "#/components/schemas/Item"}
1036 }
1037 },
1038 },
1039 "422": {
1040 "description": "Validation Error",
1041 "content": {
1042 "application/json": {
1043 "schema": {
1044 "$ref": "#/components/schemas/HTTPValidationError"
1045 }
1046 }
1047 },
1048 },
1049 },
1050 }
1051 },
1052 "/v1-to-v2/item-to-list": {
1053 "post": {
1054 "summary": "Handle V1 Item To V2 List",
1055 "operationId": "handle_v1_item_to_v2_list_v1_to_v2_item_to_list_post",
1056 "requestBody": {
1057 "content": {
1058 "application/json": {
1059 "schema": pydantic_snapshot(
1060 v2=snapshot(
1061 {
1062 "allOf": [
1063 {
1064 "$ref": "#/components/schemas/Item"
1065 }
1066 ],
1067 "title": "Data",
1068 }
1069 ),
1070 v1=snapshot(
1071 {"$ref": "#/components/schemas/Item"}
1072 ),
1073 )
1074 }
1075 },
1076 "required": True,
1077 },
1078 "responses": {
1079 "200": {
1080 "description": "Successful Response",
1081 "content": {
1082 "application/json": {
1083 "schema": {
1084 "items": {
1085 "$ref": "#/components/schemas/NewItem"
1086 },
1087 "type": "array",
1088 "title": "Response Handle V1 Item To V2 List V1 To V2 Item To List Post",
1089 }
1090 }
1091 },
1092 },
1093 "422": {
1094 "description": "Validation Error",
1095 "content": {
1096 "application/json": {
1097 "schema": {
1098 "$ref": "#/components/schemas/HTTPValidationError"
1099 }
1100 }
1101 },
1102 },
1103 },
1104 }
1105 },
1106 "/v1-to-v2/list-to-list": {
1107 "post": {
1108 "summary": "Handle V1 List To V2 List",
1109 "operationId": "handle_v1_list_to_v2_list_v1_to_v2_list_to_list_post",
1110 "requestBody": {
1111 "content": {
1112 "application/json": {
1113 "schema": {
1114 "items": {"$ref": "#/components/schemas/Item"},
1115 "type": "array",
1116 "title": "Data",
1117 }
1118 }
1119 },
1120 "required": True,
1121 },
1122 "responses": {
1123 "200": {
1124 "description": "Successful Response",
1125 "content": {
1126 "application/json": {
1127 "schema": {
1128 "items": {
1129 "$ref": "#/components/schemas/NewItem"
1130 },
1131 "type": "array",
1132 "title": "Response Handle V1 List To V2 List V1 To V2 List To List Post",
1133 }
1134 }
1135 },
1136 },
1137 "422": {
1138 "description": "Validation Error",
1139 "content": {
1140 "application/json": {
1141 "schema": {
1142 "$ref": "#/components/schemas/HTTPValidationError"
1143 }
1144 }
1145 },
1146 },
1147 },
1148 }
1149 },
1150 "/v1-to-v2/list-to-list-filter": {
1151 "post": {
1152 "summary": "Handle V1 List To V2 List Filter",
1153 "operationId": "handle_v1_list_to_v2_list_filter_v1_to_v2_list_to_list_filter_post",
1154 "requestBody": {
1155 "content": {
1156 "application/json": {
1157 "schema": {
1158 "items": {"$ref": "#/components/schemas/Item"},
1159 "type": "array",
1160 "title": "Data",
1161 }
1162 }
1163 },
1164 "required": True,
1165 },
1166 "responses": {
1167 "200": {
1168 "description": "Successful Response",
1169 "content": {
1170 "application/json": {
1171 "schema": {
1172 "items": {
1173 "$ref": "#/components/schemas/NewItem"
1174 },
1175 "type": "array",
1176 "title": "Response Handle V1 List To V2 List Filter V1 To V2 List To List Filter Post",
1177 }
1178 }
1179 },
1180 },
1181 "422": {
1182 "description": "Validation Error",
1183 "content": {
1184 "application/json": {
1185 "schema": {
1186 "$ref": "#/components/schemas/HTTPValidationError"
1187 }
1188 }
1189 },
1190 },
1191 },
1192 }
1193 },
1194 "/v1-to-v2/list-to-item": {
1195 "post": {
1196 "summary": "Handle V1 List To V2 Item",
1197 "operationId": "handle_v1_list_to_v2_item_v1_to_v2_list_to_item_post",
1198 "requestBody": {
1199 "content": {
1200 "application/json": {
1201 "schema": {
1202 "items": {"$ref": "#/components/schemas/Item"},
1203 "type": "array",
1204 "title": "Data",
1205 }
1206 }
1207 },
1208 "required": True,
1209 },
1210 "responses": {
1211 "200": {
1212 "description": "Successful Response",
1213 "content": {
1214 "application/json": {
1215 "schema": {
1216 "$ref": "#/components/schemas/NewItem"
1217 }
1218 }
1219 },
1220 },
1221 "422": {
1222 "description": "Validation Error",
1223 "content": {
1224 "application/json": {
1225 "schema": {
1226 "$ref": "#/components/schemas/HTTPValidationError"
1227 }
1228 }
1229 },
1230 },
1231 },
1232 }
1233 },
1234 "/v2-to-v1/item-to-list": {
1235 "post": {
1236 "summary": "Handle V2 Item To V1 List",
1237 "operationId": "handle_v2_item_to_v1_list_v2_to_v1_item_to_list_post",
1238 "requestBody": {
1239 "content": {
1240 "application/json": {
1241 "schema": {"$ref": "#/components/schemas/NewItem"}
1242 }
1243 },
1244 "required": True,
1245 },
1246 "responses": {
1247 "200": {
1248 "description": "Successful Response",
1249 "content": {
1250 "application/json": {
1251 "schema": {
1252 "items": {
1253 "$ref": "#/components/schemas/Item"
1254 },
1255 "type": "array",
1256 "title": "Response Handle V2 Item To V1 List V2 To V1 Item To List Post",
1257 }
1258 }
1259 },
1260 },
1261 "422": {
1262 "description": "Validation Error",
1263 "content": {
1264 "application/json": {
1265 "schema": {
1266 "$ref": "#/components/schemas/HTTPValidationError"
1267 }
1268 }
1269 },
1270 },
1271 },
1272 }
1273 },
1274 "/v2-to-v1/list-to-list": {
1275 "post": {
1276 "summary": "Handle V2 List To V1 List",
1277 "operationId": "handle_v2_list_to_v1_list_v2_to_v1_list_to_list_post",
1278 "requestBody": {
1279 "content": {
1280 "application/json": {
1281 "schema": {
1282 "items": {
1283 "$ref": "#/components/schemas/NewItem"
1284 },
1285 "type": "array",
1286 "title": "Data",
1287 }
1288 }
1289 },
1290 "required": True,
1291 },
1292 "responses": {
1293 "200": {
1294 "description": "Successful Response",
1295 "content": {
1296 "application/json": {
1297 "schema": {
1298 "items": {
1299 "$ref": "#/components/schemas/Item"
1300 },
1301 "type": "array",
1302 "title": "Response Handle V2 List To V1 List V2 To V1 List To List Post",
1303 }
1304 }
1305 },
1306 },
1307 "422": {
1308 "description": "Validation Error",
1309 "content": {
1310 "application/json": {
1311 "schema": {
1312 "$ref": "#/components/schemas/HTTPValidationError"
1313 }
1314 }
1315 },
1316 },
1317 },
1318 }
1319 },
1320 "/v2-to-v1/list-to-list-filter": {
1321 "post": {
1322 "summary": "Handle V2 List To V1 List Filter",
1323 "operationId": "handle_v2_list_to_v1_list_filter_v2_to_v1_list_to_list_filter_post",
1324 "requestBody": {
1325 "content": {
1326 "application/json": {
1327 "schema": {
1328 "items": {
1329 "$ref": "#/components/schemas/NewItem"
1330 },
1331 "type": "array",
1332 "title": "Data",
1333 }
1334 }
1335 },
1336 "required": True,
1337 },
1338 "responses": {
1339 "200": {
1340 "description": "Successful Response",
1341 "content": {
1342 "application/json": {
1343 "schema": {
1344 "items": {
1345 "$ref": "#/components/schemas/Item"
1346 },
1347 "type": "array",
1348 "title": "Response Handle V2 List To V1 List Filter V2 To V1 List To List Filter Post",
1349 }
1350 }
1351 },
1352 },
1353 "422": {
1354 "description": "Validation Error",
1355 "content": {
1356 "application/json": {
1357 "schema": {
1358 "$ref": "#/components/schemas/HTTPValidationError"
1359 }
1360 }
1361 },
1362 },
1363 },
1364 }
1365 },
1366 "/v2-to-v1/list-to-item": {
1367 "post": {
1368 "summary": "Handle V2 List To V1 Item",
1369 "operationId": "handle_v2_list_to_v1_item_v2_to_v1_list_to_item_post",
1370 "requestBody": {
1371 "content": {
1372 "application/json": {
1373 "schema": {
1374 "items": {
1375 "$ref": "#/components/schemas/NewItem"
1376 },
1377 "type": "array",
1378 "title": "Data",
1379 }
1380 }
1381 },
1382 "required": True,
1383 },
1384 "responses": {
1385 "200": {
1386 "description": "Successful Response",
1387 "content": {
1388 "application/json": {
1389 "schema": {"$ref": "#/components/schemas/Item"}
1390 }
1391 },
1392 },
1393 "422": {
1394 "description": "Validation Error",
1395 "content": {
1396 "application/json": {
1397 "schema": {
1398 "$ref": "#/components/schemas/HTTPValidationError"
1399 }
1400 }
1401 },
1402 },
1403 },
1404 }
1405 },
1406 },
1407 "components": {
1408 "schemas": {
1409 "HTTPValidationError": {
1410 "properties": {
1411 "detail": {
1412 "items": {
1413 "$ref": "#/components/schemas/ValidationError"
1414 },
1415 "type": "array",
1416 "title": "Detail",
1417 }
1418 },
1419 "type": "object",
1420 "title": "HTTPValidationError",
1421 },
1422 "Item": {
1423 "properties": {
1424 "title": {"type": "string", "title": "Title"},
1425 "size": {"type": "integer", "title": "Size"},
1426 "description": {"type": "string", "title": "Description"},
1427 "sub": {"$ref": "#/components/schemas/SubItem"},
1428 "multi": {
1429 "items": {"$ref": "#/components/schemas/SubItem"},
1430 "type": "array",
1431 "title": "Multi",
1432 "default": [],
1433 },
1434 },
1435 "type": "object",
1436 "required": ["title", "size", "sub"],
1437 "title": "Item",
1438 },
1439 "NewItem": {
1440 "properties": {
1441 "new_title": {"type": "string", "title": "New Title"},
1442 "new_size": {"type": "integer", "title": "New Size"},
1443 "new_description": pydantic_snapshot(
1444 v2=snapshot(
1445 {
1446 "anyOf": [{"type": "string"}, {"type": "null"}],
1447 "title": "New Description",
1448 }
1449 ),
1450 v1=snapshot(
1451 {"type": "string", "title": "New Description"}
1452 ),
1453 ),
1454 "new_sub": {"$ref": "#/components/schemas/NewSubItem"},
1455 "new_multi": {
1456 "items": {"$ref": "#/components/schemas/NewSubItem"},
1457 "type": "array",
1458 "title": "New Multi",
1459 "default": [],
1460 },
1461 },
1462 "type": "object",
1463 "required": ["new_title", "new_size", "new_sub"],
1464 "title": "NewItem",
1465 },
1466 "NewSubItem": {
1467 "properties": {
1468 "new_sub_name": {"type": "string", "title": "New Sub Name"}
1469 },
1470 "type": "object",
1471 "required": ["new_sub_name"],
1472 "title": "NewSubItem",
1473 },
1474 "SubItem": {
1475 "properties": {"name": {"type": "string", "title": "Name"}},
1476 "type": "object",
1477 "required": ["name"],
1478 "title": "SubItem",
1479 },
1480 "ValidationError": {
1481 "properties": {
1482 "loc": {
1483 "items": {
1484 "anyOf": [{"type": "string"}, {"type": "integer"}]
1485 },
1486 "type": "array",
1487 "title": "Location",
1488 },
1489 "msg": {"type": "string", "title": "Message"},
1490 "type": {"type": "string", "title": "Error Type"},
1491 },
1492 "type": "object",
1493 "required": ["loc", "msg", "type"],
1494 "title": "ValidationError",
1495 },
1496 }
1497 },
1498 }
1499 )