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

This commit is contained in:
Sebastián Ramírez 2026-02-22 08:34:59 -08:00 committed by GitHub
parent 2e62fb1513
commit 48e9835732
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 165 additions and 21 deletions

View File

@ -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:

View File

@ -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.
Read more about it in the **Deprecated**: `UJSONResponse` is deprecated. FastAPI now serializes data
[FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/). 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: 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.
Read more about it in the **Deprecated**: `ORJSONResponse` is deprecated. FastAPI now serializes data
[FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/). 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: def render(self, content: Any) -> bytes:

View File

@ -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",

View File

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

View File

@ -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}

View File

@ -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

View File

@ -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
View File

@ -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" },