diff --git a/README.md b/README.md index c9da5e48c..78cb0807e 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ Starlette does not have any hard dependencies, but the following are optional: * [`pyyaml`][pyyaml] - Required for `SchemaGenerator` support. * [`graphene`][graphene] - Required for `GraphQLApp` support. * [`ujson`][ujson] - Required if you want to use `UJSONResponse`. +* [`orjson`][orjson] - Required if you want to use `ORJSONResponse`. You can install all of these with `pip3 install starlette[full]`. diff --git a/docs/responses.md b/docs/responses.md index bf357a355..ffa2dfc2c 100644 --- a/docs/responses.md +++ b/docs/responses.md @@ -112,6 +112,25 @@ async def app(scope, receive, send): await response(scope, receive, send) ``` +### ORJSONResponse + +Another JSON response class that uses the optimised `orjson` library for serialisation. + +`orjson` is a fast, correct JSON library for Python. It +[benchmarks](https://github.com/ijl/orjson#performance) as the fastest Python +library for JSON and is more correct than the standard json library or other +third-party libraries. + +```python +from starlette.responses import ORJSONResponse + + +async def app(scope, receive, send): + assert scope['type'] == 'http' + response = ORJSONResponse({'hello': 'world'}) + await response(scope, receive, send) +``` + ### RedirectResponse Returns an HTTP redirect. Uses a 307 status code by default. diff --git a/requirements.txt b/requirements.txt index dc18d7897..0ea5af4ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ python-multipart pyyaml requests ujson +orjson # Testing autoflake diff --git a/setup.py b/setup.py index de80e0ef9..89651de29 100644 --- a/setup.py +++ b/setup.py @@ -58,6 +58,7 @@ def get_packages(package): "pyyaml", "requests", "ujson", + "orjson", ] }, classifiers=[ diff --git a/starlette/responses.py b/starlette/responses.py index 143e9e014..3f66b09a5 100644 --- a/starlette/responses.py +++ b/starlette/responses.py @@ -29,6 +29,11 @@ except ImportError: # pragma: nocover ujson = None # type: ignore +try: + import orjson +except ImportError: # pragma: nocover + orjson = None # type: ignore + class Response: media_type = None @@ -171,6 +176,14 @@ def render(self, content: typing.Any) -> bytes: return ujson.dumps(content, ensure_ascii=False).encode("utf-8") +class ORJSONResponse(JSONResponse): + media_type = "application/json" + + def render(self, content: typing.Any) -> bytes: + assert orjson is not None, "orjson must be installed to use ORJSONResponse" + return orjson.dumps(content) + + class RedirectResponse(Response): def __init__( self, url: typing.Union[str, URL], status_code: int = 307, headers: dict = None diff --git a/tests/test_responses.py b/tests/test_responses.py index a28bbbf52..f38b88d67 100644 --- a/tests/test_responses.py +++ b/tests/test_responses.py @@ -13,6 +13,7 @@ Response, StreamingResponse, UJSONResponse, + ORJSONResponse, ) from starlette.testclient import TestClient @@ -47,6 +48,16 @@ async def app(scope, receive, send): assert response.json() == {"hello": "world"} +def test_orjson_response(): + async def app(scope, receive, send): + response = ORJSONResponse({"hello": "world"}) + await response(scope, receive, send) + + client = TestClient(app) + response = client.get("/") + assert response.json() == {"hello": "world"} + + def test_json_none_response(): async def app(scope, receive, send): response = JSONResponse(None)