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.exceptions import FastAPIDeprecationWarning 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=FastAPIDeprecationWarning, ) @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()