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

1import sys 1abcdefQ

2from typing import Any, List, Union 1abcdefQ

3 

4from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 1abcdefQ

5 

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

7 skip_module_if_py_gte_314() 1Q

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

13 

14 

15class SubItem(BaseModel): 1abcdef

16 name: str 1abcdef

17 

18 

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

25 

26 

27app = FastAPI() 1abcdef

28 

29 

30@app.post("/item") 1abcdef

31def handle_item(data: Item) -> List[Item]: 1abcdef

32 return [data, data] 1EFGHIJ

33 

34 

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

41 

42 

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

48 

49 

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

58 

59 

60@app.post("/item-list-to-list") 1abcdef

61def handle_item_list_to_list(data: List[Item]) -> List[Item]: 1abcdef

62 return data 1yzABCD

63 

64 

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

73 

74 

75client = TestClient(app) 1abcdef

76 

77 

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 } 

101 

102 

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

130 

131 

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 } 

148 

149 

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 } 

163 

164 

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

190 

191 

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 } 

202 

203 

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 } 

245 

246 

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

267 

268 

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

276 

277 

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 

302 

303 

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 ) 

323 

324 

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 ) 

342 

343 

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 ) 

361 

362 

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 )