diff --git a/docs/en/docs/advanced/custom-response.md b/docs/en/docs/advanced/custom-response.md index e88e958657..8b4b3da339 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 JSON responses. +By default, **FastAPI** will return the responses using `JSONResponse`. You can override it by returning a `Response` directly as seen in [Return a Response directly](response-directly.md){.internal-link target=_blank}. @@ -10,27 +10,43 @@ 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. /// -## JSON Responses { #json-responses } +## Use `ORJSONResponse` { #use-orjsonresponse } + +For example, if you are squeezing performance, you can install and use `orjson` 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*. -By default FastAPI returns JSON responses. +For large responses, returning a `Response` directly is much faster than returning a dictionary. -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. +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 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 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 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. +/// -### JSON Performance { #json-performance } +/// tip -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*. +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 } @@ -138,6 +154,40 @@ 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. @@ -218,7 +268,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` with some settings. +For example, let's say that you want to use `orjson`, but with some custom settings not used in the included `ORJSONResponse` class. Let's say you want it to return indented and formatted JSON, so you want to use the orjson option `orjson.OPT_INDENT_2`. @@ -242,21 +292,13 @@ 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 `HTMLResponse` by default, in all *path operations*, instead of JSON. +In the example below, **FastAPI** will use `ORJSONResponse` by default, in all *path operations*, instead of `JSONResponse`. {* ../../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 9d58490eb1..76cc50d03c 100644 --- a/docs/en/docs/advanced/response-directly.md +++ b/docs/en/docs/advanced/response-directly.md @@ -2,23 +2,19 @@ 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. -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. +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 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`. +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. -You could also create a `JSONResponse` directly and return it. +But you can return a `JSONResponse` directly from your *path operations*. -/// 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. - -/// +It might be useful, for example, to return custom headers or cookies. ## Return a `Response` { #return-a-response } -You can return any `Response` or any sub-class of it. +In fact, you can return any `Response` or any sub-class of it. -/// info +/// tip `JSONResponse` itself is a sub-class of `Response`. @@ -60,18 +56,6 @@ 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 4f611dab05..9347192607 100644 --- a/docs/en/docs/how-to/general.md +++ b/docs/en/docs/how-to/general.md @@ -6,10 +6,6 @@ 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 c8312d92c6..51492722ae 100644 --- a/docs/en/docs/tutorial/response-model.md +++ b/docs/en/docs/tutorial/response-model.md @@ -13,7 +13,6 @@ 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 d5bc783aa0..57cb062604 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 HTMLResponse +from fastapi.responses import ORJSONResponse -app = FastAPI(default_response_class=HTMLResponse) +app = FastAPI(default_response_class=ORJSONResponse) @app.get("/items/") async def read_items(): - return "

Items

This is a list of items.

" + return [{"item_id": "Foo"}] diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index 79fba93188..0535c806f2 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -199,32 +199,6 @@ 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 528c962965..ea82ab14a3 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -271,7 +271,6 @@ 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: @@ -287,8 +286,8 @@ async def serialize_response( body=response_content, endpoint_ctx=ctx, ) - serializer = field.serialize_json if dump_json else field.serialize - return serializer( + + return field.serialize( value, include=include, exclude=exclude, @@ -444,14 +443,6 @@ 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, @@ -463,16 +454,8 @@ def get_request_handler( exclude_none=response_model_exclude_none, is_coroutine=is_coroutine, endpoint_ctx=endpoint_ctx, - dump_json=use_dump_json, ) - if use_dump_json: - response = Response( - content=content, - media_type="application/json", - **response_args, - ) - else: - response = actual_response_class(content, **response_args) + 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 deleted file mode 100644 index d41d5aa66f..0000000000 --- a/tests/test_dump_json_fast_path.py +++ /dev/null @@ -1,51 +0,0 @@ -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 a691dd3a84..5fc053c524 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001.py @@ -9,6 +9,7 @@ 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 deleted file mode 100644 index ffb005cb67..0000000000 --- a/tests/test_tutorial/test_custom_response/test_tutorial010.py +++ /dev/null @@ -1,50 +0,0 @@ -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", - } - } - }, - } - )