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

1import sys 1abcdef8

2from typing import Any, List, Union 1abcdef8

3 

4from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 1abcdef8

5 

6if sys.version_info >= (3, 14): 1abcdef8

7 skip_module_if_py_gte_314() 18

8 

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

14 

15 

16class SubItem(BaseModel): 1abcdef

17 name: str 1abcdef

18 

19 

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

26 

27 

28class NewSubItem(NewBaseModel): 1abcdef

29 new_sub_name: str 1abcdef

30 

31 

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

38 

39 

40app = FastAPI() 1abcdef

41 

42 

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 ) 

52 

53 

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

67 

68 

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 ) 

78 

79 

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

93 

94 

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

105 

106 

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

121 

122 

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

140 

141 

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!#$%'

154 

155 

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()*+,-

166 

167 

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

182 

183 

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

201 

202 

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./:;=?

215 

216 

217client = TestClient(app) 1abcdef

218 

219 

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 } 

239 

240 

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 } 

254 

255 

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

281 

282 

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 } 

302 

303 

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 } 

321 

322 

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

347 

348 

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 ] 

378 

379 

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 ] 

419 

420 

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

442 

443 

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 } 

460 

461 

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 } 

472 

473 

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 ] 

502 

503 

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 ] 

535 

536 

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

564 

565 

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 } 

590 

591 

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 } 

602 

603 

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 ) 

623 

624 

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 ) 

642 

643 

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 ) 

661 

662 

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 ) 

705 

706 

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 ) 

740 

741 

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 ) 

767 

768 

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 ) 

814 

815 

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 ) 

833 

834 

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 ) 

874 

875 

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 )