import json import sys import warnings from collections.abc import Iterator from typing import Annotated, Any import pytest from fastapi import Depends, FastAPI from fastapi.testclient import TestClient if "--codspeed" not in sys.argv: pytest.skip( "Benchmark tests are skipped by default; run with --codspeed.", allow_module_level=True, ) LARGE_ITEMS: list[dict[str, Any]] = [ { "id": i, "name": f"item-{i}", "values": list(range(25)), "meta": { "active": True, "group": i % 10, "tag": f"t{i % 5}", }, } for i in range(300) ] LARGE_METADATA: dict[str, Any] = { "source": "benchmark", "version": 1, "flags": {"a": True, "b": False, "c": True}, "notes": ["x" * 50, "y" * 50, "z" * 50], } LARGE_PAYLOAD: dict[str, Any] = {"items": LARGE_ITEMS, "metadata": LARGE_METADATA} def dep_a(): return 40 def dep_b(a: Annotated[int, Depends(dep_a)]): return a + 2 @pytest.fixture( scope="module", params=[ "pydantic-v2", "pydantic-v1", ], ) def basemodel_class(request: pytest.FixtureRequest) -> type[Any]: if request.param == "pydantic-v2": from pydantic import BaseModel return BaseModel else: from pydantic.v1 import BaseModel return BaseModel @pytest.fixture(scope="module") def app(basemodel_class: type[Any]) -> FastAPI: class ItemIn(basemodel_class): name: str value: int class ItemOut(basemodel_class): name: str value: int dep: int class LargeIn(basemodel_class): items: list[dict[str, Any]] metadata: dict[str, Any] class LargeOut(basemodel_class): items: list[dict[str, Any]] metadata: dict[str, Any] app = FastAPI() with warnings.catch_warnings(record=True): warnings.filterwarnings( "ignore", message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*", category=DeprecationWarning, ) @app.post("/sync/validated", response_model=ItemOut) def sync_validated(item: ItemIn, dep: Annotated[int, Depends(dep_b)]): return ItemOut(name=item.name, value=item.value, dep=dep) @app.get("/sync/dict-no-response-model") def sync_dict_no_response_model(): return {"name": "foo", "value": 123} @app.get("/sync/dict-with-response-model", response_model=ItemOut) def sync_dict_with_response_model( dep: Annotated[int, Depends(dep_b)], ): return {"name": "foo", "value": 123, "dep": dep} @app.get("/sync/model-no-response-model") def sync_model_no_response_model(dep: Annotated[int, Depends(dep_b)]): return ItemOut(name="foo", value=123, dep=dep) @app.get("/sync/model-with-response-model", response_model=ItemOut) def sync_model_with_response_model(dep: Annotated[int, Depends(dep_b)]): return ItemOut(name="foo", value=123, dep=dep) @app.post("/async/validated", response_model=ItemOut) async def async_validated( item: ItemIn, dep: Annotated[int, Depends(dep_b)], ): return ItemOut(name=item.name, value=item.value, dep=dep) @app.post("/sync/large-receive") def sync_large_receive(payload: LargeIn): return {"received": len(payload.items)} @app.post("/async/large-receive") async def async_large_receive(payload: LargeIn): return {"received": len(payload.items)} @app.get("/sync/large-dict-no-response-model") def sync_large_dict_no_response_model(): return LARGE_PAYLOAD @app.get("/sync/large-dict-with-response-model", response_model=LargeOut) def sync_large_dict_with_response_model(): return LARGE_PAYLOAD @app.get("/sync/large-model-no-response-model") def sync_large_model_no_response_model(): return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) @app.get("/sync/large-model-with-response-model", response_model=LargeOut) def sync_large_model_with_response_model(): return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) @app.get("/async/large-dict-no-response-model") async def async_large_dict_no_response_model(): return LARGE_PAYLOAD @app.get("/async/large-dict-with-response-model", response_model=LargeOut) async def async_large_dict_with_response_model(): return LARGE_PAYLOAD @app.get("/async/large-model-no-response-model") async def async_large_model_no_response_model(): return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) @app.get("/async/large-model-with-response-model", response_model=LargeOut) async def async_large_model_with_response_model(): return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) @app.get("/async/dict-no-response-model") async def async_dict_no_response_model(): return {"name": "foo", "value": 123} @app.get("/async/dict-with-response-model", response_model=ItemOut) async def async_dict_with_response_model( dep: Annotated[int, Depends(dep_b)], ): return {"name": "foo", "value": 123, "dep": dep} @app.get("/async/model-no-response-model") async def async_model_no_response_model( dep: Annotated[int, Depends(dep_b)], ): return ItemOut(name="foo", value=123, dep=dep) @app.get("/async/model-with-response-model", response_model=ItemOut) async def async_model_with_response_model( dep: Annotated[int, Depends(dep_b)], ): return ItemOut(name="foo", value=123, dep=dep) return app @pytest.fixture(scope="module") def client(app: FastAPI) -> Iterator[TestClient]: with TestClient(app) as client: yield client def _bench_get(benchmark, client: TestClient, path: str) -> tuple[int, bytes]: warmup = client.get(path) assert warmup.status_code == 200 def do_request() -> tuple[int, bytes]: response = client.get(path) return response.status_code, response.content return benchmark(do_request) def _bench_post_json( benchmark, client: TestClient, path: str, json: dict[str, Any] ) -> tuple[int, bytes]: warmup = client.post(path, json=json) assert warmup.status_code == 200 def do_request() -> tuple[int, bytes]: response = client.post(path, json=json) return response.status_code, response.content return benchmark(do_request) def test_sync_receiving_validated_pydantic_model(benchmark, client: TestClient) -> None: status_code, body = _bench_post_json( benchmark, client, "/sync/validated", json={"name": "foo", "value": 123}, ) assert status_code == 200 assert body == b'{"name":"foo","value":123,"dep":42}' def test_sync_return_dict_without_response_model(benchmark, client: TestClient) -> None: status_code, body = _bench_get(benchmark, client, "/sync/dict-no-response-model") assert status_code == 200 assert body == b'{"name":"foo","value":123}' def test_sync_return_dict_with_response_model(benchmark, client: TestClient) -> None: status_code, body = _bench_get(benchmark, client, "/sync/dict-with-response-model") assert status_code == 200 assert body == b'{"name":"foo","value":123,"dep":42}' def test_sync_return_model_without_response_model( benchmark, client: TestClient ) -> None: status_code, body = _bench_get(benchmark, client, "/sync/model-no-response-model") assert status_code == 200 assert body == b'{"name":"foo","value":123,"dep":42}' def test_sync_return_model_with_response_model(benchmark, client: TestClient) -> None: status_code, body = _bench_get(benchmark, client, "/sync/model-with-response-model") assert status_code == 200 assert body == b'{"name":"foo","value":123,"dep":42}' def test_async_receiving_validated_pydantic_model( benchmark, client: TestClient ) -> None: status_code, body = _bench_post_json( benchmark, client, "/async/validated", json={"name": "foo", "value": 123} ) assert status_code == 200 assert body == b'{"name":"foo","value":123,"dep":42}' def test_async_return_dict_without_response_model( benchmark, client: TestClient ) -> None: status_code, body = _bench_get(benchmark, client, "/async/dict-no-response-model") assert status_code == 200 assert body == b'{"name":"foo","value":123}' def test_async_return_dict_with_response_model(benchmark, client: TestClient) -> None: status_code, body = _bench_get(benchmark, client, "/async/dict-with-response-model") assert status_code == 200 assert body == b'{"name":"foo","value":123,"dep":42}' def test_async_return_model_without_response_model( benchmark, client: TestClient ) -> None: status_code, body = _bench_get(benchmark, client, "/async/model-no-response-model") assert status_code == 200 assert body == b'{"name":"foo","value":123,"dep":42}' def test_async_return_model_with_response_model(benchmark, client: TestClient) -> None: status_code, body = _bench_get( benchmark, client, "/async/model-with-response-model" ) assert status_code == 200 assert body == b'{"name":"foo","value":123,"dep":42}' def test_sync_receiving_large_payload(benchmark, client: TestClient) -> None: status_code, body = _bench_post_json( benchmark, client, "/sync/large-receive", json=LARGE_PAYLOAD, ) assert status_code == 200 assert body == b'{"received":300}' def test_async_receiving_large_payload(benchmark, client: TestClient) -> None: status_code, body = _bench_post_json( benchmark, client, "/async/large-receive", json=LARGE_PAYLOAD, ) assert status_code == 200 assert body == b'{"received":300}' def _expected_large_payload_json_bytes() -> bytes: return json.dumps( LARGE_PAYLOAD, ensure_ascii=False, allow_nan=False, separators=(",", ":"), ).encode("utf-8") def test_sync_return_large_dict_without_response_model( benchmark, client: TestClient ) -> None: status_code, body = _bench_get( benchmark, client, "/sync/large-dict-no-response-model" ) assert status_code == 200 assert body == _expected_large_payload_json_bytes() def test_sync_return_large_dict_with_response_model( benchmark, client: TestClient ) -> None: status_code, body = _bench_get( benchmark, client, "/sync/large-dict-with-response-model" ) assert status_code == 200 assert body == _expected_large_payload_json_bytes() def test_sync_return_large_model_without_response_model( benchmark, client: TestClient ) -> None: status_code, body = _bench_get( benchmark, client, "/sync/large-model-no-response-model" ) assert status_code == 200 assert body == _expected_large_payload_json_bytes() def test_sync_return_large_model_with_response_model( benchmark, client: TestClient ) -> None: status_code, body = _bench_get( benchmark, client, "/sync/large-model-with-response-model" ) assert status_code == 200 assert body == _expected_large_payload_json_bytes() def test_async_return_large_dict_without_response_model( benchmark, client: TestClient ) -> None: status_code, body = _bench_get( benchmark, client, "/async/large-dict-no-response-model" ) assert status_code == 200 assert body == _expected_large_payload_json_bytes() def test_async_return_large_dict_with_response_model( benchmark, client: TestClient ) -> None: status_code, body = _bench_get( benchmark, client, "/async/large-dict-with-response-model" ) assert status_code == 200 assert body == _expected_large_payload_json_bytes() def test_async_return_large_model_without_response_model( benchmark, client: TestClient ) -> None: status_code, body = _bench_get( benchmark, client, "/async/large-model-no-response-model" ) assert status_code == 200 assert body == _expected_large_payload_json_bytes() def test_async_return_large_model_with_response_model( benchmark, client: TestClient ) -> None: status_code, body = _bench_get( benchmark, client, "/async/large-model-with-response-model" ) assert status_code == 200 assert body == _expected_large_payload_json_bytes()