diff --git a/docs/en/docs/advanced/custom-response.md b/docs/en/docs/advanced/custom-response.md index 8b4b3da339..e88e958657 100644 --- a/docs/en/docs/advanced/custom-response.md +++ b/docs/en/docs/advanced/custom-response.md @@ -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 } +## JSON Responses { #json-responses } -For example, if you are squeezing performance, you can install and use `orjson` and set the response to be `ORJSONResponse`. +By default FastAPI returns JSON responses. -Import the `Response` class (sub-class) you want to use and declare it in the *path operation decorator*. +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. -For large responses, returning a `Response` directly is much faster than returning a dictionary. +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`. -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_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. -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. +### JSON Performance { #json-performance } -{* ../../docs_src/custom_response/tutorial001b_py310.py hl[2,7] *} +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*. -/// 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. - -/// - -/// tip - -The `ORJSONResponse` is only available in FastAPI, not in Starlette. - -/// +{* ../../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 `orjson`, as you read above. - -/// info - -This requires installing `orjson` for example with `pip install orjson`. - -/// - -### `UJSONResponse` { #ujsonresponse } - -An alternative JSON response using `ujson`. - -/// 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 `orjson`, but with some custom settings not used in the included `ORJSONResponse` class. +For example, let's say that you want to use `orjson` 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] *} diff --git a/docs/en/docs/advanced/response-directly.md b/docs/en/docs/advanced/response-directly.md index 76cc50d03c..9d58490eb1 100644 --- a/docs/en/docs/advanced/response-directly.md +++ b/docs/en/docs/advanced/response-directly.md @@ -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. diff --git a/docs/en/docs/how-to/general.md b/docs/en/docs/how-to/general.md index 9347192607..4f611dab05 100644 --- a/docs/en/docs/how-to/general.md +++ b/docs/en/docs/how-to/general.md @@ -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}. diff --git a/docs/en/docs/tutorial/response-model.md b/docs/en/docs/tutorial/response-model.md index 51492722ae..c8312d92c6 100644 --- a/docs/en/docs/tutorial/response-model.md +++ b/docs/en/docs/tutorial/response-model.md @@ -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: diff --git a/docs_src/custom_response/tutorial010_py310.py b/docs_src/custom_response/tutorial010_py310.py index 57cb062604..d5bc783aa0 100644 --- a/docs_src/custom_response/tutorial010_py310.py +++ b/docs_src/custom_response/tutorial010_py310.py @@ -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 "

Items

This is a list of items.

" diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index 0535c806f2..79fba93188 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -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. diff --git a/fastapi/routing.py b/fastapi/routing.py index ea82ab14a3..528c962965 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -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 index 0000000000..d41d5aa66f --- /dev/null +++ b/tests/test_dump_json_fast_path.py @@ -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() diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001.py b/tests/test_tutorial/test_custom_response/test_tutorial001.py index a5fe4c8f4c..cec5ebe6cb 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001.py @@ -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 index 0000000000..ffb005cb67 --- /dev/null +++ b/tests/test_tutorial/test_custom_response/test_tutorial010.py @@ -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("

Items

This is a list of items.

") + + +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", + } + } + }, + } + )