diff --git a/docs/en/docs/reference/responses.md b/docs/en/docs/reference/responses.md index bd5786129..2df53e970 100644 --- a/docs/en/docs/reference/responses.md +++ b/docs/en/docs/reference/responses.md @@ -22,7 +22,13 @@ from fastapi.responses import ( ## FastAPI Responses -There are a couple of custom FastAPI response classes, you can use them to optimize JSON performance. +There were a couple of custom FastAPI response classes that were intended to optimize JSON performance. + +However, they are now deprecated as you will now get better performance by using a [Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/). + +That way, Pydantic will serialize the data into JSON bytes on the Rust side, which will achieve better performance than these custom JSON responses. + +Read more about it in [Custom Response - HTML, Stream, File, others - `orjson` or Response Model](https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model). ::: fastapi.responses.UJSONResponse options: diff --git a/fastapi/responses.py b/fastapi/responses.py index 6c8db6f33..5b1154c04 100644 --- a/fastapi/responses.py +++ b/fastapi/responses.py @@ -1,5 +1,6 @@ from typing import Any +from fastapi.exceptions import FastAPIDeprecationWarning from starlette.responses import FileResponse as FileResponse # noqa from starlette.responses import HTMLResponse as HTMLResponse # noqa from starlette.responses import JSONResponse as JSONResponse # noqa @@ -7,6 +8,7 @@ from starlette.responses import PlainTextResponse as PlainTextResponse # noqa from starlette.responses import RedirectResponse as RedirectResponse # noqa from starlette.responses import Response as Response # noqa from starlette.responses import StreamingResponse as StreamingResponse # noqa +from typing_extensions import deprecated try: import ujson @@ -20,12 +22,29 @@ except ImportError: # pragma: nocover orjson = None # type: ignore +@deprecated( + "UJSONResponse is deprecated, FastAPI now serializes data directly to JSON " + "bytes via Pydantic when a return type or response model is set, which is " + "faster and doesn't need a custom response class. Read more in the FastAPI " + "docs: https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model " + "and https://fastapi.tiangolo.com/tutorial/response-model/", + category=FastAPIDeprecationWarning, + stacklevel=2, +) class UJSONResponse(JSONResponse): - """ - JSON response using the high-performance ujson library to serialize data to JSON. + """JSON response using the ujson library to serialize data to JSON. - Read more about it in the - [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/). + **Deprecated**: `UJSONResponse` is deprecated. FastAPI now serializes data + directly to JSON bytes via Pydantic when a return type or response model is + set, which is faster and doesn't need a custom response class. + + Read more in the + [FastAPI docs for Custom Response](https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model) + and the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + + **Note**: `ujson` is not included with FastAPI and must be installed + separately, e.g. `pip install ujson`. """ def render(self, content: Any) -> bytes: @@ -33,12 +52,29 @@ class UJSONResponse(JSONResponse): return ujson.dumps(content, ensure_ascii=False).encode("utf-8") +@deprecated( + "ORJSONResponse is deprecated, FastAPI now serializes data directly to JSON " + "bytes via Pydantic when a return type or response model is set, which is " + "faster and doesn't need a custom response class. Read more in the FastAPI " + "docs: https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model " + "and https://fastapi.tiangolo.com/tutorial/response-model/", + category=FastAPIDeprecationWarning, + stacklevel=2, +) class ORJSONResponse(JSONResponse): - """ - JSON response using the high-performance orjson library to serialize data to JSON. + """JSON response using the orjson library to serialize data to JSON. - Read more about it in the - [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/). + **Deprecated**: `ORJSONResponse` is deprecated. FastAPI now serializes data + directly to JSON bytes via Pydantic when a return type or response model is + set, which is faster and doesn't need a custom response class. + + Read more in the + [FastAPI docs for Custom Response](https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model) + and the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + + **Note**: `orjson` is not included with FastAPI and must be installed + separately, e.g. `pip install orjson`. """ def render(self, content: Any) -> bytes: diff --git a/pyproject.toml b/pyproject.toml index c51eb8ce9..79dfc1fd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,10 +105,6 @@ all = [ "itsdangerous >=1.1.0", # For Starlette's schema generation, would not be used with FastAPI "pyyaml >=5.3.1", - # For UJSONResponse - "ujson >=5.8.0", - # For ORJSONResponse - "orjson >=3.9.3", # To validate email fields "email-validator >=2.0.0", # Uvicorn with uvloop @@ -151,6 +147,10 @@ docs = [ docs-tests = [ "httpx >=0.23.0,<1.0.0", "ruff >=0.14.14", + # For UJSONResponse + "ujson >=5.8.0", + # For ORJSONResponse + "orjson >=3.9.3", ] github-actions = [ "httpx >=0.27.0,<1.0.0", diff --git a/tests/test_deprecated_responses.py b/tests/test_deprecated_responses.py new file mode 100644 index 000000000..eff579271 --- /dev/null +++ b/tests/test_deprecated_responses.py @@ -0,0 +1,73 @@ +import warnings + +import pytest +from fastapi import FastAPI +from fastapi.exceptions import FastAPIDeprecationWarning +from fastapi.responses import ORJSONResponse, UJSONResponse +from fastapi.testclient import TestClient +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + price: float + + +# ORJSON + + +def _make_orjson_app() -> FastAPI: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", FastAPIDeprecationWarning) + app = FastAPI(default_response_class=ORJSONResponse) + + @app.get("/items") + def get_items() -> Item: + return Item(name="widget", price=9.99) + + return app + + +def test_orjson_response_returns_correct_data(): + app = _make_orjson_app() + client = TestClient(app) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", FastAPIDeprecationWarning) + response = client.get("/items") + assert response.status_code == 200 + assert response.json() == {"name": "widget", "price": 9.99} + + +def test_orjson_response_emits_deprecation_warning(): + with pytest.warns(FastAPIDeprecationWarning, match="ORJSONResponse is deprecated"): + ORJSONResponse(content={"hello": "world"}) + + +# UJSON + + +def _make_ujson_app() -> FastAPI: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", FastAPIDeprecationWarning) + app = FastAPI(default_response_class=UJSONResponse) + + @app.get("/items") + def get_items() -> Item: + return Item(name="widget", price=9.99) + + return app + + +def test_ujson_response_returns_correct_data(): + app = _make_ujson_app() + client = TestClient(app) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", FastAPIDeprecationWarning) + response = client.get("/items") + assert response.status_code == 200 + assert response.json() == {"name": "widget", "price": 9.99} + + +def test_ujson_response_emits_deprecation_warning(): + with pytest.warns(FastAPIDeprecationWarning, match="UJSONResponse is deprecated"): + UJSONResponse(content={"hello": "world"}) diff --git a/tests/test_orjson_response_class.py b/tests/test_orjson_response_class.py index 6fe62daf9..63ea054d1 100644 --- a/tests/test_orjson_response_class.py +++ b/tests/test_orjson_response_class.py @@ -1,9 +1,14 @@ +import warnings + from fastapi import FastAPI +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.responses import ORJSONResponse from fastapi.testclient import TestClient from sqlalchemy.sql.elements import quoted_name -app = FastAPI(default_response_class=ORJSONResponse) +with warnings.catch_warnings(): + warnings.simplefilter("ignore", FastAPIDeprecationWarning) + app = FastAPI(default_response_class=ORJSONResponse) @app.get("/orjson_non_str_keys") @@ -16,6 +21,8 @@ client = TestClient(app) def test_orjson_non_str_keys(): - with client: - response = client.get("/orjson_non_str_keys") + with warnings.catch_warnings(): + warnings.simplefilter("ignore", FastAPIDeprecationWarning) + with client: + response = client.get("/orjson_non_str_keys") assert response.json() == {"msg": "Hello World", "1": 1} diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001.py b/tests/test_tutorial/test_custom_response/test_tutorial001.py index cec5ebe6c..a691dd3a8 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001.py @@ -17,12 +17,14 @@ def get_client(request: pytest.FixtureRequest): return client +@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning") def test_get_custom_response(client: TestClient): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] +@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning") def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001b.py b/tests/test_tutorial/test_custom_response/test_tutorial001b.py index 32437db86..11ce813b7 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001b.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001b.py @@ -1,17 +1,25 @@ +import warnings + +import pytest +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient from inline_snapshot import snapshot -from docs_src.custom_response.tutorial001b_py310 import app +with warnings.catch_warnings(): + warnings.simplefilter("ignore", FastAPIDeprecationWarning) + from docs_src.custom_response.tutorial001b_py310 import app client = TestClient(app) +@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning") def test_get_custom_response(): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] +@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning") def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/uv.lock b/uv.lock index 15ca8714f..0d16c930b 100644 --- a/uv.lock +++ b/uv.lock @@ -1083,12 +1083,10 @@ all = [ { name = "httpx" }, { name = "itsdangerous" }, { name = "jinja2" }, - { name = "orjson" }, { name = "pydantic-extra-types" }, { name = "pydantic-settings" }, { name = "python-multipart" }, { name = "pyyaml" }, - { name = "ujson" }, { name = "uvicorn", extra = ["standard"] }, ] standard = [ @@ -1134,6 +1132,7 @@ dev = [ { name = "mkdocs-redirects" }, { name = "mkdocstrings", extra = ["python"] }, { name = "mypy" }, + { name = "orjson" }, { name = "pillow" }, { name = "playwright" }, { name = "prek" }, @@ -1151,6 +1150,7 @@ dev = [ { name = "typer" }, { name = "types-orjson" }, { name = "types-ujson" }, + { name = "ujson" }, ] docs = [ { name = "black" }, @@ -1165,15 +1165,19 @@ docs = [ { name = "mkdocs-material" }, { name = "mkdocs-redirects" }, { name = "mkdocstrings", extra = ["python"] }, + { name = "orjson" }, { name = "pillow" }, { name = "python-slugify" }, { name = "pyyaml" }, { name = "ruff" }, { name = "typer" }, + { name = "ujson" }, ] docs-tests = [ { name = "httpx" }, + { name = "orjson" }, { name = "ruff" }, + { name = "ujson" }, ] github-actions = [ { name = "httpx" }, @@ -1192,6 +1196,7 @@ tests = [ { name = "httpx" }, { name = "inline-snapshot" }, { name = "mypy" }, + { name = "orjson" }, { name = "pwdlib", extra = ["argon2"] }, { name = "pyjwt" }, { name = "pytest" }, @@ -1202,6 +1207,7 @@ tests = [ { name = "strawberry-graphql" }, { name = "types-orjson" }, { name = "types-ujson" }, + { name = "ujson" }, ] translations = [ { name = "gitpython" }, @@ -1225,7 +1231,6 @@ requires-dist = [ { name = "jinja2", marker = "extra == 'all'", specifier = ">=3.1.5" }, { name = "jinja2", marker = "extra == 'standard'", specifier = ">=3.1.5" }, { name = "jinja2", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=3.1.5" }, - { name = "orjson", marker = "extra == 'all'", specifier = ">=3.9.3" }, { name = "pydantic", specifier = ">=2.7.0" }, { name = "pydantic-extra-types", marker = "extra == 'all'", specifier = ">=2.0.0" }, { name = "pydantic-extra-types", marker = "extra == 'standard'", specifier = ">=2.0.0" }, @@ -1240,7 +1245,6 @@ requires-dist = [ { name = "starlette", specifier = ">=0.40.0,<1.0.0" }, { name = "typing-extensions", specifier = ">=4.8.0" }, { name = "typing-inspection", specifier = ">=0.4.2" }, - { name = "ujson", marker = "extra == 'all'", specifier = ">=5.8.0" }, { name = "uvicorn", extras = ["standard"], marker = "extra == 'all'", specifier = ">=0.12.0" }, { name = "uvicorn", extras = ["standard"], marker = "extra == 'standard'", specifier = ">=0.12.0" }, { name = "uvicorn", extras = ["standard"], marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.12.0" }, @@ -1269,6 +1273,7 @@ dev = [ { name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" }, { name = "mypy", specifier = ">=1.14.1" }, + { name = "orjson", specifier = ">=3.9.3" }, { name = "pillow", specifier = ">=11.3.0" }, { name = "playwright", specifier = ">=1.57.0" }, { name = "prek", specifier = ">=0.2.22" }, @@ -1286,6 +1291,7 @@ dev = [ { name = "typer", specifier = ">=0.21.1" }, { name = "types-orjson", specifier = ">=3.6.2" }, { name = "types-ujson", specifier = ">=5.10.0.20240515" }, + { name = "ujson", specifier = ">=5.8.0" }, ] docs = [ { name = "black", specifier = ">=25.1.0" }, @@ -1300,15 +1306,19 @@ docs = [ { name = "mkdocs-material", specifier = ">=9.7.0" }, { name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" }, + { name = "orjson", specifier = ">=3.9.3" }, { name = "pillow", specifier = ">=11.3.0" }, { name = "python-slugify", specifier = ">=8.0.4" }, { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, { name = "ruff", specifier = ">=0.14.14" }, { name = "typer", specifier = ">=0.21.1" }, + { name = "ujson", specifier = ">=5.8.0" }, ] docs-tests = [ { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, + { name = "orjson", specifier = ">=3.9.3" }, { name = "ruff", specifier = ">=0.14.14" }, + { name = "ujson", specifier = ">=5.8.0" }, ] github-actions = [ { name = "httpx", specifier = ">=0.27.0,<1.0.0" }, @@ -1327,6 +1337,7 @@ tests = [ { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, { name = "inline-snapshot", specifier = ">=0.21.1" }, { name = "mypy", specifier = ">=1.14.1" }, + { name = "orjson", specifier = ">=3.9.3" }, { name = "pwdlib", extras = ["argon2"], specifier = ">=0.2.1" }, { name = "pyjwt", specifier = ">=2.9.0" }, { name = "pytest", specifier = ">=9.0.0" }, @@ -1337,6 +1348,7 @@ tests = [ { name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" }, { name = "types-orjson", specifier = ">=3.6.2" }, { name = "types-ujson", specifier = ">=5.10.0.20240515" }, + { name = "ujson", specifier = ">=5.8.0" }, ] translations = [ { name = "gitpython", specifier = ">=3.1.46" },