Skip to content

Commit 8384ef3

Browse files
committed
feat: using orjson replace ujson
1 parent 3587895 commit 8384ef3

File tree

8 files changed

+358
-16
lines changed

8 files changed

+358
-16
lines changed

CHANGES/10795.feature.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added :py:mod:`orjson` support as the default JSON encoder for :py:class:`~aiohttp.ClientSession` and :py:class:`~aiohttp.JsonPayload`
2+
-- by :user:`fatelei`

aiohttp/client.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import base64
55
import dataclasses
66
import hashlib
7-
import json
87
import os
98
import sys
109
import traceback
@@ -107,7 +106,16 @@
107106
from .http import WS_KEY, HttpVersion, WebSocketReader, WebSocketWriter
108107
from .http_websocket import WSHandshakeError, ws_ext_gen, ws_ext_parse
109108
from .tracing import Trace, TraceConfig
110-
from .typedefs import JSONEncoder, LooseCookies, LooseHeaders, Query, StrOrURL
109+
from .typedefs import (
110+
DEFAULT_JSON_BYTES_ENCODER,
111+
DEFAULT_JSON_ENCODER,
112+
JSONBytesEncoder,
113+
JSONEncoder,
114+
LooseCookies,
115+
LooseHeaders,
116+
Query,
117+
StrOrURL,
118+
)
111119

112120
__all__ = (
113121
# client_exceptions
@@ -277,7 +285,8 @@ def __init__(
277285
proxy_auth: Optional[BasicAuth] = None,
278286
skip_auto_headers: Optional[Iterable[str]] = None,
279287
auth: Optional[BasicAuth] = None,
280-
json_serialize: JSONEncoder = json.dumps,
288+
json_serialize: JSONEncoder = DEFAULT_JSON_ENCODER,
289+
json_serialize_bytes: JSONBytesEncoder = DEFAULT_JSON_BYTES_ENCODER,
281290
request_class: Type[ClientRequest] = ClientRequest,
282291
response_class: Type[ClientResponse] = ClientResponse,
283292
ws_response_class: Type[ClientWebSocketResponse] = ClientWebSocketResponse,
@@ -357,6 +366,7 @@ def __init__(
357366
self._default_auth = auth
358367
self._version = version
359368
self._json_serialize = json_serialize
369+
self._json_serialize_bytes = json_serialize_bytes
360370
self._raise_for_status = raise_for_status
361371
self._auto_decompress = auto_decompress
362372
self._trust_env = trust_env
@@ -484,7 +494,11 @@ async def _request(
484494
"data and json parameters can not be used at the same time"
485495
)
486496
elif json is not None:
487-
data = payload.JsonPayload(json, dumps=self._json_serialize)
497+
data = payload.JsonPayload(
498+
json,
499+
dumps=self._json_serialize,
500+
dumps_bytes=self._json_serialize_bytes,
501+
)
488502

489503
redirects = 0
490504
history: List[ClientResponse] = []
@@ -1316,6 +1330,11 @@ def json_serialize(self) -> JSONEncoder:
13161330
"""Json serializer callable"""
13171331
return self._json_serialize
13181332

1333+
@property
1334+
def json_serialize_bytes(self) -> JSONBytesEncoder:
1335+
"""Json bytes serializer callable"""
1336+
return self._json_serialize_bytes
1337+
13191338
@property
13201339
def connector_owner(self) -> bool:
13211340
"""Should connector be closed on session closing"""

aiohttp/payload.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import asyncio
22
import enum
33
import io
4-
import json
54
import mimetypes
65
import os
76
import sys
@@ -36,7 +35,13 @@
3635
sentinel,
3736
)
3837
from .streams import StreamReader
39-
from .typedefs import JSONEncoder, _CIMultiDict
38+
from .typedefs import (
39+
DEFAULT_JSON_BYTES_ENCODER,
40+
DEFAULT_JSON_ENCODER,
41+
JSONBytesEncoder,
42+
JSONEncoder,
43+
_CIMultiDict,
44+
)
4045

4146
__all__ = (
4247
"PAYLOAD_REGISTRY",
@@ -939,15 +944,17 @@ def __init__(
939944
value: Any,
940945
encoding: str = "utf-8",
941946
content_type: str = "application/json",
942-
dumps: JSONEncoder = json.dumps,
943-
*args: Any,
947+
dumps: JSONEncoder = DEFAULT_JSON_ENCODER,
948+
*,
949+
dumps_bytes: JSONBytesEncoder = DEFAULT_JSON_BYTES_ENCODER,
944950
**kwargs: Any,
945951
) -> None:
952+
# Prefer bytes serializer to avoid extra encode/decode
953+
body = dumps_bytes(value)
946954
super().__init__(
947-
dumps(value).encode(encoding),
955+
body,
948956
content_type=content_type,
949957
encoding=encoding,
950-
*args,
951958
**kwargs,
952959
)
953960

aiohttp/typedefs.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,33 @@
1717

1818
Query = _Query
1919

20-
DEFAULT_JSON_ENCODER = json.dumps
21-
DEFAULT_JSON_DECODER = json.loads
20+
# Try to use orjson for better performance, fallback to standard json
21+
try:
22+
import orjson
23+
24+
def _orjson_dumps(obj: Any) -> str:
25+
"""orjson encoder that returns str (like json.dumps)."""
26+
return orjson.dumps(obj).decode("utf-8")
27+
28+
def _orjson_dumps_bytes(obj: Any) -> bytes:
29+
"""orjson encoder that returns bytes directly (fast path)."""
30+
return orjson.dumps(obj)
31+
32+
def _orjson_loads(s: str) -> Any:
33+
"""orjson decoder that accepts str (like json.loads)."""
34+
return orjson.loads(s)
35+
36+
DEFAULT_JSON_ENCODER = _orjson_dumps
37+
DEFAULT_JSON_DECODER = _orjson_loads
38+
DEFAULT_JSON_BYTES_ENCODER = _orjson_dumps_bytes
39+
except ImportError:
40+
DEFAULT_JSON_ENCODER = json.dumps
41+
DEFAULT_JSON_DECODER = json.loads
42+
43+
def _json_dumps_bytes_fallback(obj: Any) -> bytes:
44+
return json.dumps(obj).encode("utf-8")
45+
46+
DEFAULT_JSON_BYTES_ENCODER = _json_dumps_bytes_fallback
2247

2348
if TYPE_CHECKING:
2449
_CIMultiDict = CIMultiDict[str]
@@ -37,6 +62,7 @@
3762
Byteish = Union[bytes, bytearray, memoryview]
3863
JSONEncoder = Callable[[Any], str]
3964
JSONDecoder = Callable[[str], Any]
65+
JSONBytesEncoder = Callable[[Any], bytes]
4066
LooseHeaders = Union[
4167
Mapping[str, str],
4268
Mapping[istr, str],

docs/client_quickstart.rst

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,16 +210,21 @@ serialization. But it is possible to use different
210210
``serializer``. :class:`ClientSession` accepts ``json_serialize``
211211
parameter::
212212

213-
import ujson
213+
import orjson
214214

215215
async with aiohttp.ClientSession(
216-
json_serialize=ujson.dumps) as session:
216+
json_serialize=orjson.dumps) as session:
217217
await session.post(url, json={'test': 'object'})
218218

219219
.. note::
220220

221-
``ujson`` library is faster than standard :mod:`json` but slightly
222-
incompatible.
221+
``orjson`` library is much faster than standard :mod:`json` and is now
222+
the default when available. You can install it with the ``speedups`` extra:
223+
``pip install aiohttp[speedups]`` or separately with ``pip install orjson``.
224+
``ujson`` was previously recommended but is now deprecated in favor of
225+
``orjson`` due to security and maintenance concerns.
226+
If ``orjson`` is not available, aiohttp will fall back to the standard
227+
:mod:`json` module.
223228

224229
JSON Response Content
225230
=====================

requirements/runtime-deps.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Brotli; platform_python_implementation == 'CPython'
88
brotlicffi; platform_python_implementation != 'CPython'
99
frozenlist >= 1.1.1
1010
multidict >=4.5, < 7.0
11+
orjson >= 3.8.0 ; platform_python_implementation == "CPython"
1112
propcache >= 0.2.0
1213
yarl >= 1.17.0, < 2.0
1314
zstandard; platform_python_implementation == 'CPython' and python_version < "3.14"

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ install_requires =
5858
multidict >=4.5, < 7.0
5959
propcache >= 0.2.0
6060
yarl >= 1.17.0, < 2.0
61+
orjson >= 3.8.0
6162

6263
[options.exclude_package_data]
6364
* =

0 commit comments

Comments
 (0)