Browse Source

🗑️ Deprecate `ORJSONResponse` and `UJSONResponse` (#14964)

pull/14965/head
Sebastián Ramírez 3 months ago
committed by GitHub
parent
commit
48e9835732
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 8
      docs/en/docs/reference/responses.md
  2. 52
      fastapi/responses.py
  3. 8
      pyproject.toml
  4. 73
      tests/test_deprecated_responses.py
  5. 13
      tests/test_orjson_response_class.py
  6. 2
      tests/test_tutorial/test_custom_response/test_tutorial001.py
  7. 10
      tests/test_tutorial/test_custom_response/test_tutorial001b.py
  8. 20
      uv.lock

8
docs/en/docs/reference/responses.md

@ -22,7 +22,13 @@ from fastapi.responses import (
## FastAPI Responses ## 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 ::: fastapi.responses.UJSONResponse
options: options:

52
fastapi/responses.py

@ -1,5 +1,6 @@
from typing import Any from typing import Any
from fastapi.exceptions import FastAPIDeprecationWarning
from starlette.responses import FileResponse as FileResponse # noqa from starlette.responses import FileResponse as FileResponse # noqa
from starlette.responses import HTMLResponse as HTMLResponse # noqa from starlette.responses import HTMLResponse as HTMLResponse # noqa
from starlette.responses import JSONResponse as JSONResponse # 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 RedirectResponse as RedirectResponse # noqa
from starlette.responses import Response as Response # noqa from starlette.responses import Response as Response # noqa
from starlette.responses import StreamingResponse as StreamingResponse # noqa from starlette.responses import StreamingResponse as StreamingResponse # noqa
from typing_extensions import deprecated
try: try:
import ujson import ujson
@ -20,12 +22,29 @@ except ImportError: # pragma: nocover
orjson = None # type: ignore 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): class UJSONResponse(JSONResponse):
""" """JSON response using the ujson library to serialize data to JSON.
JSON response using the high-performance ujson library to serialize data to JSON.
**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/).
Read more about it in the **Note**: `ujson` is not included with FastAPI and must be installed
[FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/). separately, e.g. `pip install ujson`.
""" """
def render(self, content: Any) -> bytes: def render(self, content: Any) -> bytes:
@ -33,12 +52,29 @@ class UJSONResponse(JSONResponse):
return ujson.dumps(content, ensure_ascii=False).encode("utf-8") 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): class ORJSONResponse(JSONResponse):
""" """JSON response using the orjson library to serialize data to JSON.
JSON response using the high-performance orjson library to serialize data to JSON.
**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/).
Read more about it in the **Note**: `orjson` is not included with FastAPI and must be installed
[FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/). separately, e.g. `pip install orjson`.
""" """
def render(self, content: Any) -> bytes: def render(self, content: Any) -> bytes:

8
pyproject.toml

@ -105,10 +105,6 @@ all = [
"itsdangerous >=1.1.0", "itsdangerous >=1.1.0",
# For Starlette's schema generation, would not be used with FastAPI # For Starlette's schema generation, would not be used with FastAPI
"pyyaml >=5.3.1", "pyyaml >=5.3.1",
# For UJSONResponse
"ujson >=5.8.0",
# For ORJSONResponse
"orjson >=3.9.3",
# To validate email fields # To validate email fields
"email-validator >=2.0.0", "email-validator >=2.0.0",
# Uvicorn with uvloop # Uvicorn with uvloop
@ -151,6 +147,10 @@ docs = [
docs-tests = [ docs-tests = [
"httpx >=0.23.0,<1.0.0", "httpx >=0.23.0,<1.0.0",
"ruff >=0.14.14", "ruff >=0.14.14",
# For UJSONResponse
"ujson >=5.8.0",
# For ORJSONResponse
"orjson >=3.9.3",
] ]
github-actions = [ github-actions = [
"httpx >=0.27.0,<1.0.0", "httpx >=0.27.0,<1.0.0",

73
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"})

13
tests/test_orjson_response_class.py

@ -1,9 +1,14 @@
import warnings
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.exceptions import FastAPIDeprecationWarning
from fastapi.responses import ORJSONResponse from fastapi.responses import ORJSONResponse
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlalchemy.sql.elements import quoted_name 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") @app.get("/orjson_non_str_keys")
@ -16,6 +21,8 @@ client = TestClient(app)
def test_orjson_non_str_keys(): def test_orjson_non_str_keys():
with client: with warnings.catch_warnings():
response = client.get("/orjson_non_str_keys") warnings.simplefilter("ignore", FastAPIDeprecationWarning)
with client:
response = client.get("/orjson_non_str_keys")
assert response.json() == {"msg": "Hello World", "1": 1} assert response.json() == {"msg": "Hello World", "1": 1}

2
tests/test_tutorial/test_custom_response/test_tutorial001.py

@ -17,12 +17,14 @@ def get_client(request: pytest.FixtureRequest):
return client return client
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
def test_get_custom_response(client: TestClient): def test_get_custom_response(client: TestClient):
response = client.get("/items/") response = client.get("/items/")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
assert response.json() == [{"item_id": "Foo"}] assert response.json() == [{"item_id": "Foo"}]
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
def test_openapi_schema(client: TestClient): def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text

10
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 fastapi.testclient import TestClient
from inline_snapshot import snapshot 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) client = TestClient(app)
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
def test_get_custom_response(): def test_get_custom_response():
response = client.get("/items/") response = client.get("/items/")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
assert response.json() == [{"item_id": "Foo"}] assert response.json() == [{"item_id": "Foo"}]
@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning")
def test_openapi_schema(): def test_openapi_schema():
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text

20
uv.lock

@ -1083,12 +1083,10 @@ all = [
{ name = "httpx" }, { name = "httpx" },
{ name = "itsdangerous" }, { name = "itsdangerous" },
{ name = "jinja2" }, { name = "jinja2" },
{ name = "orjson" },
{ name = "pydantic-extra-types" }, { name = "pydantic-extra-types" },
{ name = "pydantic-settings" }, { name = "pydantic-settings" },
{ name = "python-multipart" }, { name = "python-multipart" },
{ name = "pyyaml" }, { name = "pyyaml" },
{ name = "ujson" },
{ name = "uvicorn", extra = ["standard"] }, { name = "uvicorn", extra = ["standard"] },
] ]
standard = [ standard = [
@ -1134,6 +1132,7 @@ dev = [
{ name = "mkdocs-redirects" }, { name = "mkdocs-redirects" },
{ name = "mkdocstrings", extra = ["python"] }, { name = "mkdocstrings", extra = ["python"] },
{ name = "mypy" }, { name = "mypy" },
{ name = "orjson" },
{ name = "pillow" }, { name = "pillow" },
{ name = "playwright" }, { name = "playwright" },
{ name = "prek" }, { name = "prek" },
@ -1151,6 +1150,7 @@ dev = [
{ name = "typer" }, { name = "typer" },
{ name = "types-orjson" }, { name = "types-orjson" },
{ name = "types-ujson" }, { name = "types-ujson" },
{ name = "ujson" },
] ]
docs = [ docs = [
{ name = "black" }, { name = "black" },
@ -1165,15 +1165,19 @@ docs = [
{ name = "mkdocs-material" }, { name = "mkdocs-material" },
{ name = "mkdocs-redirects" }, { name = "mkdocs-redirects" },
{ name = "mkdocstrings", extra = ["python"] }, { name = "mkdocstrings", extra = ["python"] },
{ name = "orjson" },
{ name = "pillow" }, { name = "pillow" },
{ name = "python-slugify" }, { name = "python-slugify" },
{ name = "pyyaml" }, { name = "pyyaml" },
{ name = "ruff" }, { name = "ruff" },
{ name = "typer" }, { name = "typer" },
{ name = "ujson" },
] ]
docs-tests = [ docs-tests = [
{ name = "httpx" }, { name = "httpx" },
{ name = "orjson" },
{ name = "ruff" }, { name = "ruff" },
{ name = "ujson" },
] ]
github-actions = [ github-actions = [
{ name = "httpx" }, { name = "httpx" },
@ -1192,6 +1196,7 @@ tests = [
{ name = "httpx" }, { name = "httpx" },
{ name = "inline-snapshot" }, { name = "inline-snapshot" },
{ name = "mypy" }, { name = "mypy" },
{ name = "orjson" },
{ name = "pwdlib", extra = ["argon2"] }, { name = "pwdlib", extra = ["argon2"] },
{ name = "pyjwt" }, { name = "pyjwt" },
{ name = "pytest" }, { name = "pytest" },
@ -1202,6 +1207,7 @@ tests = [
{ name = "strawberry-graphql" }, { name = "strawberry-graphql" },
{ name = "types-orjson" }, { name = "types-orjson" },
{ name = "types-ujson" }, { name = "types-ujson" },
{ name = "ujson" },
] ]
translations = [ translations = [
{ name = "gitpython" }, { name = "gitpython" },
@ -1225,7 +1231,6 @@ requires-dist = [
{ name = "jinja2", marker = "extra == 'all'", specifier = ">=3.1.5" }, { name = "jinja2", marker = "extra == 'all'", specifier = ">=3.1.5" },
{ name = "jinja2", marker = "extra == 'standard'", 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 = "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", specifier = ">=2.7.0" },
{ name = "pydantic-extra-types", marker = "extra == 'all'", specifier = ">=2.0.0" }, { name = "pydantic-extra-types", marker = "extra == 'all'", specifier = ">=2.0.0" },
{ name = "pydantic-extra-types", marker = "extra == 'standard'", 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 = "starlette", specifier = ">=0.40.0,<1.0.0" },
{ name = "typing-extensions", specifier = ">=4.8.0" }, { name = "typing-extensions", specifier = ">=4.8.0" },
{ name = "typing-inspection", specifier = ">=0.4.2" }, { 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 == 'all'", specifier = ">=0.12.0" },
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'standard'", 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" }, { 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 = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" },
{ name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" },
{ name = "mypy", specifier = ">=1.14.1" }, { name = "mypy", specifier = ">=1.14.1" },
{ name = "orjson", specifier = ">=3.9.3" },
{ name = "pillow", specifier = ">=11.3.0" }, { name = "pillow", specifier = ">=11.3.0" },
{ name = "playwright", specifier = ">=1.57.0" }, { name = "playwright", specifier = ">=1.57.0" },
{ name = "prek", specifier = ">=0.2.22" }, { name = "prek", specifier = ">=0.2.22" },
@ -1286,6 +1291,7 @@ dev = [
{ name = "typer", specifier = ">=0.21.1" }, { name = "typer", specifier = ">=0.21.1" },
{ name = "types-orjson", specifier = ">=3.6.2" }, { name = "types-orjson", specifier = ">=3.6.2" },
{ name = "types-ujson", specifier = ">=5.10.0.20240515" }, { name = "types-ujson", specifier = ">=5.10.0.20240515" },
{ name = "ujson", specifier = ">=5.8.0" },
] ]
docs = [ docs = [
{ name = "black", specifier = ">=25.1.0" }, { name = "black", specifier = ">=25.1.0" },
@ -1300,15 +1306,19 @@ docs = [
{ name = "mkdocs-material", specifier = ">=9.7.0" }, { name = "mkdocs-material", specifier = ">=9.7.0" },
{ name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" }, { name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" },
{ name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" },
{ name = "orjson", specifier = ">=3.9.3" },
{ name = "pillow", specifier = ">=11.3.0" }, { name = "pillow", specifier = ">=11.3.0" },
{ name = "python-slugify", specifier = ">=8.0.4" }, { name = "python-slugify", specifier = ">=8.0.4" },
{ name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" },
{ name = "ruff", specifier = ">=0.14.14" }, { name = "ruff", specifier = ">=0.14.14" },
{ name = "typer", specifier = ">=0.21.1" }, { name = "typer", specifier = ">=0.21.1" },
{ name = "ujson", specifier = ">=5.8.0" },
] ]
docs-tests = [ docs-tests = [
{ name = "httpx", specifier = ">=0.23.0,<1.0.0" }, { name = "httpx", specifier = ">=0.23.0,<1.0.0" },
{ name = "orjson", specifier = ">=3.9.3" },
{ name = "ruff", specifier = ">=0.14.14" }, { name = "ruff", specifier = ">=0.14.14" },
{ name = "ujson", specifier = ">=5.8.0" },
] ]
github-actions = [ github-actions = [
{ name = "httpx", specifier = ">=0.27.0,<1.0.0" }, { name = "httpx", specifier = ">=0.27.0,<1.0.0" },
@ -1327,6 +1337,7 @@ tests = [
{ name = "httpx", specifier = ">=0.23.0,<1.0.0" }, { name = "httpx", specifier = ">=0.23.0,<1.0.0" },
{ name = "inline-snapshot", specifier = ">=0.21.1" }, { name = "inline-snapshot", specifier = ">=0.21.1" },
{ name = "mypy", specifier = ">=1.14.1" }, { name = "mypy", specifier = ">=1.14.1" },
{ name = "orjson", specifier = ">=3.9.3" },
{ name = "pwdlib", extras = ["argon2"], specifier = ">=0.2.1" }, { name = "pwdlib", extras = ["argon2"], specifier = ">=0.2.1" },
{ name = "pyjwt", specifier = ">=2.9.0" }, { name = "pyjwt", specifier = ">=2.9.0" },
{ name = "pytest", specifier = ">=9.0.0" }, { name = "pytest", specifier = ">=9.0.0" },
@ -1337,6 +1348,7 @@ tests = [
{ name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" }, { name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" },
{ name = "types-orjson", specifier = ">=3.6.2" }, { name = "types-orjson", specifier = ">=3.6.2" },
{ name = "types-ujson", specifier = ">=5.10.0.20240515" }, { name = "types-ujson", specifier = ">=5.10.0.20240515" },
{ name = "ujson", specifier = ">=5.8.0" },
] ]
translations = [ translations = [
{ name = "gitpython", specifier = ">=3.1.46" }, { name = "gitpython", specifier = ">=3.1.46" },

Loading…
Cancel
Save