Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES/10795.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added :py:mod:`orjson` support as the default JSON encoder for :py:class:`~aiohttp.ClientSession` and :py:class:`~aiohttp.JsonPayload`
-- by :user:`fatelei`
27 changes: 23 additions & 4 deletions aiohttp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import base64
import dataclasses
import hashlib
import json
import os
import sys
import traceback
Expand Down Expand Up @@ -107,7 +106,16 @@
from .http import WS_KEY, HttpVersion, WebSocketReader, WebSocketWriter
from .http_websocket import WSHandshakeError, ws_ext_gen, ws_ext_parse
from .tracing import Trace, TraceConfig
from .typedefs import JSONEncoder, LooseCookies, LooseHeaders, Query, StrOrURL
from .typedefs import (
DEFAULT_JSON_BYTES_ENCODER,
DEFAULT_JSON_ENCODER,
JSONBytesEncoder,
JSONEncoder,
LooseCookies,
LooseHeaders,
Query,
StrOrURL,
)

__all__ = (
# client_exceptions
Expand Down Expand Up @@ -277,7 +285,8 @@ def __init__(
proxy_auth: Optional[BasicAuth] = None,
skip_auto_headers: Optional[Iterable[str]] = None,
auth: Optional[BasicAuth] = None,
json_serialize: JSONEncoder = json.dumps,
json_serialize: JSONEncoder = DEFAULT_JSON_ENCODER,
json_serialize_bytes: JSONBytesEncoder = DEFAULT_JSON_BYTES_ENCODER,
request_class: Type[ClientRequest] = ClientRequest,
response_class: Type[ClientResponse] = ClientResponse,
ws_response_class: Type[ClientWebSocketResponse] = ClientWebSocketResponse,
Expand Down Expand Up @@ -357,6 +366,7 @@ def __init__(
self._default_auth = auth
self._version = version
self._json_serialize = json_serialize
self._json_serialize_bytes = json_serialize_bytes
self._raise_for_status = raise_for_status
self._auto_decompress = auto_decompress
self._trust_env = trust_env
Expand Down Expand Up @@ -484,7 +494,11 @@ async def _request(
"data and json parameters can not be used at the same time"
)
elif json is not None:
data = payload.JsonPayload(json, dumps=self._json_serialize)
data = payload.JsonPayload(
json,
dumps=self._json_serialize,
dumps_bytes=self._json_serialize_bytes,
)

redirects = 0
history: List[ClientResponse] = []
Expand Down Expand Up @@ -1316,6 +1330,11 @@ def json_serialize(self) -> JSONEncoder:
"""Json serializer callable"""
return self._json_serialize

@property
def json_serialize_bytes(self) -> JSONBytesEncoder:
"""Json bytes serializer callable"""
return self._json_serialize_bytes

@property
def connector_owner(self) -> bool:
"""Should connector be closed on session closing"""
Expand Down
19 changes: 13 additions & 6 deletions aiohttp/payload.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import asyncio
import enum
import io
import json
import mimetypes
import os
import sys
Expand Down Expand Up @@ -36,7 +35,13 @@
sentinel,
)
from .streams import StreamReader
from .typedefs import JSONEncoder, _CIMultiDict
from .typedefs import (
DEFAULT_JSON_BYTES_ENCODER,
DEFAULT_JSON_ENCODER,
JSONBytesEncoder,
JSONEncoder,
_CIMultiDict,
)

__all__ = (
"PAYLOAD_REGISTRY",
Expand Down Expand Up @@ -939,15 +944,17 @@ def __init__(
value: Any,
encoding: str = "utf-8",
content_type: str = "application/json",
dumps: JSONEncoder = json.dumps,
*args: Any,
dumps: JSONEncoder = DEFAULT_JSON_ENCODER,
*,
dumps_bytes: JSONBytesEncoder = DEFAULT_JSON_BYTES_ENCODER,
**kwargs: Any,
) -> None:
# Prefer bytes serializer to avoid extra encode/decode
body = dumps_bytes(value)
super().__init__(
dumps(value).encode(encoding),
body,
content_type=content_type,
encoding=encoding,
*args,
**kwargs,
)

Expand Down
30 changes: 28 additions & 2 deletions aiohttp/typedefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,33 @@

Query = _Query

DEFAULT_JSON_ENCODER = json.dumps
DEFAULT_JSON_DECODER = json.loads
# Try to use orjson for better performance, fallback to standard json
try:
import orjson

def _orjson_dumps(obj: Any) -> str:
"""orjson encoder that returns str (like json.dumps)."""
return orjson.dumps(obj).decode("utf-8")

def _orjson_dumps_bytes(obj: Any) -> bytes:
"""orjson encoder that returns bytes directly (fast path)."""
return orjson.dumps(obj)

def _orjson_loads(s: str) -> Any:
"""orjson decoder that accepts str (like json.loads)."""
return orjson.loads(s)

DEFAULT_JSON_ENCODER = _orjson_dumps
DEFAULT_JSON_DECODER = _orjson_loads
DEFAULT_JSON_BYTES_ENCODER = _orjson_dumps_bytes
except ImportError:
DEFAULT_JSON_ENCODER = json.dumps
DEFAULT_JSON_DECODER = json.loads

def _json_dumps_bytes_fallback(obj: Any) -> bytes:
return json.dumps(obj).encode("utf-8")

DEFAULT_JSON_BYTES_ENCODER = _json_dumps_bytes_fallback

if TYPE_CHECKING:
_CIMultiDict = CIMultiDict[str]
Expand All @@ -37,6 +62,7 @@
Byteish = Union[bytes, bytearray, memoryview]
JSONEncoder = Callable[[Any], str]
JSONDecoder = Callable[[str], Any]
JSONBytesEncoder = Callable[[Any], bytes]
LooseHeaders = Union[
Mapping[str, str],
Mapping[istr, str],
Expand Down
13 changes: 9 additions & 4 deletions docs/client_quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -210,16 +210,21 @@ serialization. But it is possible to use different
``serializer``. :class:`ClientSession` accepts ``json_serialize``
parameter::

import ujson
import orjson

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

.. note::

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

JSON Response Content
=====================
Expand Down
1 change: 1 addition & 0 deletions requirements/runtime-deps.in
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Brotli; platform_python_implementation == 'CPython'
brotlicffi; platform_python_implementation != 'CPython'
frozenlist >= 1.1.1
multidict >=4.5, < 7.0
orjson >= 3.8.0 ; platform_python_implementation == "CPython"
propcache >= 0.2.0
yarl >= 1.17.0, < 2.0
zstandard; platform_python_implementation == 'CPython' and python_version < "3.14"
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ install_requires =
multidict >=4.5, < 7.0
propcache >= 0.2.0
yarl >= 1.17.0, < 2.0
orjson >= 3.8.0

[options.exclude_package_data]
* =
Expand Down
Loading
Loading