]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
✨ Serialize JSON response with Pydantic (in Rust), when there's a Pydantic return...
authorSebastián Ramírez <tiangolo@gmail.com>
Sun, 22 Feb 2026 16:07:19 +0000 (08:07 -0800)
committerGitHub <noreply@github.com>
Sun, 22 Feb 2026 16:07:19 +0000 (17:07 +0100)
docs/en/docs/advanced/custom-response.md
docs/en/docs/advanced/response-directly.md
docs/en/docs/how-to/general.md
docs/en/docs/tutorial/response-model.md
docs_src/custom_response/tutorial010_py310.py
fastapi/_compat/v2.py
fastapi/routing.py
tests/test_dump_json_fast_path.py [new file with mode: 0644]
tests/test_tutorial/test_custom_response/test_tutorial001.py
tests/test_tutorial/test_custom_response/test_tutorial010.py [new file with mode: 0644]

index 8b4b3da339e7cfad2c3ad12ebc8dc641307da9b0..e88e95865784244e458701db4856630fac2c3afd 100644 (file)
@@ -1,6 +1,6 @@
 # Custom Response - HTML, Stream, File, others { #custom-response-html-stream-file-others }
 
-By default, **FastAPI** will return the responses using `JSONResponse`.
+By default, **FastAPI** will return JSON responses.
 
 You can override it by returning a `Response` directly as seen in [Return a Response directly](response-directly.md){.internal-link target=_blank}.
 
@@ -10,43 +10,27 @@ But you can also declare the `Response` that you want to be used (e.g. any `Resp
 
 The contents that you return from your *path operation function* will be put inside of that `Response`.
 
-And if that `Response` has a JSON media type (`application/json`), like is the case with the `JSONResponse` and `UJSONResponse`, the data you return will be automatically converted (and filtered) with any Pydantic `response_model` that you declared in the *path operation decorator*.
-
 /// note
 
 If you use a response class with no media type, FastAPI will expect your response to have no content, so it will not document the response format in its generated OpenAPI docs.
 
 ///
 
-## Use `ORJSONResponse` { #use-orjsonresponse }
-
-For example, if you are squeezing performance, you can install and use <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a> and set the response to be `ORJSONResponse`.
-
-Import the `Response` class (sub-class) you want to use and declare it in the *path operation decorator*.
+## JSON Responses { #json-responses }
 
-For large responses, returning a `Response` directly is much faster than returning a dictionary.
+By default FastAPI returns JSON responses.
 
-This is because by default, FastAPI will inspect every item inside and make sure it is serializable as JSON, using the same [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} explained in the tutorial. This is what allows you to return **arbitrary objects**, for example database models.
+If you declare a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} FastAPI will use it to serialize the data to JSON, using Pydantic.
 
-But if you are certain that the content that you are returning is **serializable with JSON**, you can pass it directly to the response class and avoid the extra overhead that FastAPI would have by passing your return content through the `jsonable_encoder` before passing it to the response class.
-
-{* ../../docs_src/custom_response/tutorial001b_py310.py hl[2,7] *}
-
-/// info
-
-The parameter `response_class` will also be used to define the "media type" of the response.
-
-In this case, the HTTP header `Content-Type` will be set to `application/json`.
-
-And it will be documented as such in OpenAPI.
+If you don't declare a response model, FastAPI will use the `jsonable_encoder` explained in [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} and put it in a `JSONResponse`.
 
-///
+If you declare a `response_class` with a JSON media type (`application/json`), like is the case with the `JSONResponse`, the data you return will be automatically converted (and filtered) with any Pydantic `response_model` that you declared in the *path operation decorator*. But the data won't be serialized to JSON bytes with Pydantic, instead it will be converted with the `jsonable_encoder` and then passed to the `JSONResponse` class, which will serialize it to bytes using the standard JSON library in Python.
 
-/// tip
+### JSON Performance { #json-performance }
 
-The `ORJSONResponse` is only available in FastAPI, not in Starlette.
+In short, if you want the maximum performance, use a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} and don't declare a `response_class` in the *path operation decorator*.
 
-///
+{* ../../docs_src/response_model/tutorial001_01_py310.py ln[15:17] hl[16] *}
 
 ## HTML Response { #html-response }
 
@@ -154,40 +138,6 @@ Takes some data and returns an `application/json` encoded response.
 
 This is the default response used in **FastAPI**, as you read above.
 
-### `ORJSONResponse` { #orjsonresponse }
-
-A fast alternative JSON response using <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a>, as you read above.
-
-/// info
-
-This requires installing `orjson` for example with `pip install orjson`.
-
-///
-
-### `UJSONResponse` { #ujsonresponse }
-
-An alternative JSON response using <a href="https://github.com/ultrajson/ultrajson" class="external-link" target="_blank">`ujson`</a>.
-
-/// info
-
-This requires installing `ujson` for example with `pip install ujson`.
-
-///
-
-/// warning
-
-`ujson` is less careful than Python's built-in implementation in how it handles some edge-cases.
-
-///
-
-{* ../../docs_src/custom_response/tutorial001_py310.py hl[2,7] *}
-
-/// tip
-
-It's possible that `ORJSONResponse` might be a faster alternative.
-
-///
-
 ### `RedirectResponse` { #redirectresponse }
 
 Returns an HTTP redirect. Uses a 307 status code (Temporary Redirect) by default.
@@ -268,7 +218,7 @@ In this case, you can return the file path directly from your *path operation* f
 
 You can create your own custom response class, inheriting from `Response` and using it.
 
-For example, let's say that you want to use <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a>, but with some custom settings not used in the included `ORJSONResponse` class.
+For example, let's say that you want to use <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a> with some settings.
 
 Let's say you want it to return indented and formatted JSON, so you want to use the orjson option `orjson.OPT_INDENT_2`.
 
@@ -292,13 +242,21 @@ Now instead of returning:
 
 Of course, you will probably find much better ways to take advantage of this than formatting JSON. 😉
 
+### `orjson` or Response Model { #orjson-or-response-model }
+
+If what you are looking for is performance, you are probably better off using a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} than an `orjson` response.
+
+With a response model, FastAPI will use Pydantic to serialize the data to JSON, without using intermediate steps, like converting it with `jsonable_encoder`, which would happen in any other case.
+
+And under the hood, Pydantic uses the same underlying Rust mechanisms as `orjson` to serialize to JSON, so you will already get the best performance with a response model.
+
 ## Default response class { #default-response-class }
 
 When creating a **FastAPI** class instance or an `APIRouter` you can specify which response class to use by default.
 
 The parameter that defines this is `default_response_class`.
 
-In the example below, **FastAPI** will use `ORJSONResponse` by default, in all *path operations*, instead of `JSONResponse`.
+In the example below, **FastAPI** will use `HTMLResponse` by default, in all *path operations*, instead of JSON.
 
 {* ../../docs_src/custom_response/tutorial010_py310.py hl[2,4] *}
 
index 76cc50d03c6add858e3f9aa239db7f77a512aa1b..9d58490eb11ec170603f5ca5747ff3a465e32852 100644 (file)
@@ -2,19 +2,23 @@
 
 When you create a **FastAPI** *path operation* you can normally return any data from it: a `dict`, a `list`, a Pydantic model, a database model, etc.
 
-By default, **FastAPI** would automatically convert that return value to JSON using the `jsonable_encoder` explained in [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank}.
+If you declare a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} FastAPI will use it to serialize the data to JSON, using Pydantic.
 
-Then, behind the scenes, it would put that JSON-compatible data (e.g. a `dict`) inside of a `JSONResponse` that would be used to send the response to the client.
+If you don't declare a response model, FastAPI will use the `jsonable_encoder` explained in [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} and put it in a `JSONResponse`.
 
-But you can return a `JSONResponse` directly from your *path operations*.
+You could also create a `JSONResponse` directly and return it.
 
-It might be useful, for example, to return custom headers or cookies.
+/// tip
+
+You will normally have much better performance using a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} than returning a `JSONResponse` directly, as that way it serializes the data using Pydantic, in Rust.
+
+///
 
 ## Return a `Response` { #return-a-response }
 
-In fact, you can return any `Response` or any sub-class of it.
+You can return any `Response` or any sub-class of it.
 
-/// tip
+/// info
 
 `JSONResponse` itself is a sub-class of `Response`.
 
@@ -56,6 +60,18 @@ You could put your XML content in a string, put that in a `Response`, and return
 
 {* ../../docs_src/response_directly/tutorial002_py310.py hl[1,18] *}
 
+## How a Response Model Works { #how-a-response-model-works }
+
+When you declare a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} in a path operation, **FastAPI** will use it to serialize the data to JSON, using Pydantic.
+
+{* ../../docs_src/response_model/tutorial001_01_py310.py hl[16,21] *}
+
+As that will happen on the Rust side, the performance will be much better than if it was done with regular Python and the `JSONResponse` class.
+
+When using a response model FastAPI won't use the `jsonable_encoder` to convert the data (which would be slower) nor the `JSONResponse` class.
+
+Instead it takes the JSON bytes generated with Pydantic using the response model and returns a `Response` with the right media type for JSON directly (`application/json`).
+
 ## Notes { #notes }
 
 When you return a `Response` directly its data is not validated, converted (serialized), or documented automatically.
index 9347192607a986fe210a16aba1beb95dbe1d74cd..4f611dab05dd093d86a57aefebd1cfb8c02fa593 100644 (file)
@@ -6,6 +6,10 @@ Here are several pointers to other places in the docs, for general or frequent q
 
 To ensure that you don't return more data than you should, read the docs for [Tutorial - Response Model - Return Type](../tutorial/response-model.md){.internal-link target=_blank}.
 
+## Optimize Response Performance - Response Model - Return Type { #optimize-response-performance-response-model-return-type }
+
+To optimize performance when returning JSON data, use a return type or response model, that way Pydantic will handle the serialization to JSON on the Rust side, without going through Python. Read more in the docs for [Tutorial - Response Model - Return Type](../tutorial/response-model.md){.internal-link target=_blank}.
+
 ## Documentation Tags - OpenAPI { #documentation-tags-openapi }
 
 To add tags to your *path operations*, and group them in the docs UI, read the docs for [Tutorial - Path Operation Configurations - Tags](../tutorial/path-operation-configuration.md#tags){.internal-link target=_blank}.
index 51492722ae2f6d54ddcb0261cc786336d60c4883..c8312d92c6e74af940be1eb0ad36437f0255a37f 100644 (file)
@@ -13,6 +13,7 @@ FastAPI will use this return type to:
 * Add a **JSON Schema** for the response, in the OpenAPI *path operation*.
     * This will be used by the **automatic docs**.
     * It will also be used by automatic client code generation tools.
+* **Serialize** the returned data to JSON using Pydantic, which is written in **Rust**, so it will be **much faster**.
 
 But most importantly:
 
index 57cb0626040de03bcd36ee4dd35d95ff146459c7..d5bc783aa0f7fba9ca1344bccb89b15c50c61818 100644 (file)
@@ -1,9 +1,9 @@
 from fastapi import FastAPI
-from fastapi.responses import ORJSONResponse
+from fastapi.responses import HTMLResponse
 
-app = FastAPI(default_response_class=ORJSONResponse)
+app = FastAPI(default_response_class=HTMLResponse)
 
 
 @app.get("/items/")
 async def read_items():
-    return [{"item_id": "Foo"}]
+    return "<h1>Items</h1><p>This is a list of items.</p>"
index 0535c806f293d812d01a99e0be4abdd6499a698d..79fba931881e1d775e0525f5ca0a1942beec47a9 100644 (file)
@@ -199,6 +199,32 @@ class ModelField:
             exclude_none=exclude_none,
         )
 
+    def serialize_json(
+        self,
+        value: Any,
+        *,
+        include: IncEx | None = None,
+        exclude: IncEx | None = None,
+        by_alias: bool = True,
+        exclude_unset: bool = False,
+        exclude_defaults: bool = False,
+        exclude_none: bool = False,
+    ) -> bytes:
+        # What calls this code passes a value that already called
+        # self._type_adapter.validate_python(value)
+        # This uses Pydantic's dump_json() which serializes directly to JSON
+        # bytes in one pass (via Rust), avoiding the intermediate Python dict
+        # step of dump_python(mode="json") + json.dumps().
+        return self._type_adapter.dump_json(
+            value,
+            include=include,
+            exclude=exclude,
+            by_alias=by_alias,
+            exclude_unset=exclude_unset,
+            exclude_defaults=exclude_defaults,
+            exclude_none=exclude_none,
+        )
+
     def __hash__(self) -> int:
         # Each ModelField is unique for our purposes, to allow making a dict from
         # ModelField to its JSON Schema.
index ea82ab14a3529961b23808f5c83c71d36d3562bf..528c962965f856feb0298e814fb24e9d4d4fc00c 100644 (file)
@@ -271,6 +271,7 @@ async def serialize_response(
     exclude_none: bool = False,
     is_coroutine: bool = True,
     endpoint_ctx: EndpointContext | None = None,
+    dump_json: bool = False,
 ) -> Any:
     if field:
         if is_coroutine:
@@ -286,8 +287,8 @@ async def serialize_response(
                 body=response_content,
                 endpoint_ctx=ctx,
             )
-
-        return field.serialize(
+        serializer = field.serialize_json if dump_json else field.serialize
+        return serializer(
             value,
             include=include,
             exclude=exclude,
@@ -443,6 +444,14 @@ def get_request_handler(
                     response_args["status_code"] = current_status_code
                 if solved_result.response.status_code:
                     response_args["status_code"] = solved_result.response.status_code
+                # Use the fast path (dump_json) when no custom response
+                # class was set and a response field with a TypeAdapter
+                # exists. Serializes directly to JSON bytes via Pydantic's
+                # Rust core, skipping the intermediate Python dict +
+                # json.dumps() step.
+                use_dump_json = response_field is not None and isinstance(
+                    response_class, DefaultPlaceholder
+                )
                 content = await serialize_response(
                     field=response_field,
                     response_content=raw_response,
@@ -454,8 +463,16 @@ def get_request_handler(
                     exclude_none=response_model_exclude_none,
                     is_coroutine=is_coroutine,
                     endpoint_ctx=endpoint_ctx,
+                    dump_json=use_dump_json,
                 )
-                response = actual_response_class(content, **response_args)
+                if use_dump_json:
+                    response = Response(
+                        content=content,
+                        media_type="application/json",
+                        **response_args,
+                    )
+                else:
+                    response = actual_response_class(content, **response_args)
                 if not is_body_allowed_for_status_code(response.status_code):
                     response.body = b""
                 response.headers.raw.extend(solved_result.response.headers.raw)
diff --git a/tests/test_dump_json_fast_path.py b/tests/test_dump_json_fast_path.py
new file mode 100644 (file)
index 0000000..d41d5aa
--- /dev/null
@@ -0,0 +1,51 @@
+from unittest.mock import patch
+
+from fastapi import FastAPI
+from fastapi.responses import JSONResponse
+from fastapi.testclient import TestClient
+from pydantic import BaseModel
+
+
+class Item(BaseModel):
+    name: str
+    price: float
+
+
+app = FastAPI()
+
+
+@app.get("/default")
+def get_default() -> Item:
+    return Item(name="widget", price=9.99)
+
+
+@app.get("/explicit", response_class=JSONResponse)
+def get_explicit() -> Item:
+    return Item(name="widget", price=9.99)
+
+
+client = TestClient(app)
+
+
+def test_default_response_class_skips_json_dumps():
+    """When no response_class is set, the fast path serializes directly to
+    JSON bytes via Pydantic's dump_json and never calls json.dumps."""
+    with patch(
+        "starlette.responses.json.dumps", wraps=__import__("json").dumps
+    ) as mock_dumps:
+        response = client.get("/default")
+    assert response.status_code == 200
+    assert response.json() == {"name": "widget", "price": 9.99}
+    mock_dumps.assert_not_called()
+
+
+def test_explicit_response_class_uses_json_dumps():
+    """When response_class is explicitly set to JSONResponse, the normal path
+    is used and json.dumps is called via JSONResponse.render()."""
+    with patch(
+        "starlette.responses.json.dumps", wraps=__import__("json").dumps
+    ) as mock_dumps:
+        response = client.get("/explicit")
+    assert response.status_code == 200
+    assert response.json() == {"name": "widget", "price": 9.99}
+    mock_dumps.assert_called_once()
index a5fe4c8f4c014b2d4ca12cc068f684dd6099046d..cec5ebe6cb80bcccc7853d9e413f48aa7f451ae9 100644 (file)
@@ -9,7 +9,6 @@ from inline_snapshot import snapshot
     name="client",
     params=[
         pytest.param("tutorial001_py310"),
-        pytest.param("tutorial010_py310"),
     ],
 )
 def get_client(request: pytest.FixtureRequest):
diff --git a/tests/test_tutorial/test_custom_response/test_tutorial010.py b/tests/test_tutorial/test_custom_response/test_tutorial010.py
new file mode 100644 (file)
index 0000000..ffb005c
--- /dev/null
@@ -0,0 +1,50 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+from inline_snapshot import snapshot
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial010_py310"),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.custom_response.{request.param}")
+    client = TestClient(mod.app)
+    return client
+
+
+def test_get_custom_response(client: TestClient):
+    response = client.get("/items/")
+    assert response.status_code == 200, response.text
+    assert response.text == snapshot("<h1>Items</h1><p>This is a list of items.</p>")
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == snapshot(
+        {
+            "openapi": "3.1.0",
+            "info": {"title": "FastAPI", "version": "0.1.0"},
+            "paths": {
+                "/items/": {
+                    "get": {
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "text/html": {"schema": {"type": "string"}}
+                                },
+                            }
+                        },
+                        "summary": "Read Items",
+                        "operationId": "read_items_items__get",
+                    }
+                }
+            },
+        }
+    )