From 47e7babc0c6059359972a163ac9a061835dcdd3e Mon Sep 17 00:00:00 2001 From: Jason Tung Date: Mon, 3 Nov 2025 17:08:06 -0500 Subject: [PATCH 1/4] allow for default json decoder to be set at session level. --- aiohttp/client.py | 48 ++++++++++++++++++++++++++++++---------- aiohttp/client_reqrep.py | 11 +++++++-- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/aiohttp/client.py b/aiohttp/client.py index 026006023ce..8ac52b7f127 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -4,7 +4,6 @@ import base64 import dataclasses import hashlib -import json import os import sys import traceback @@ -96,7 +95,15 @@ 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, StrOrURL +from .typedefs import ( + DEFAULT_JSON_DECODER, + DEFAULT_JSON_ENCODER, + JSONDecoder, + JSONEncoder, + LooseCookies, + LooseHeaders, + StrOrURL, +) __all__ = ( # client_exceptions @@ -234,6 +241,7 @@ class ClientSession: "_default_auth", "_version", "_json_serialize", + "_json_deserialize", "_requote_redirect_url", "_timeout", "_raise_for_status", @@ -266,7 +274,8 @@ def __init__( proxy_auth: BasicAuth | None = None, skip_auto_headers: Iterable[str] | None = None, auth: BasicAuth | None = None, - json_serialize: JSONEncoder = json.dumps, + json_serialize: JSONEncoder = DEFAULT_JSON_ENCODER, + json_deserialize: JSONDecoder = DEFAULT_JSON_DECODER, request_class: type[ClientRequest] = ClientRequest, response_class: type[ClientResponse] = ClientResponse, ws_response_class: type[ClientWebSocketResponse] = ClientWebSocketResponse, @@ -344,6 +353,7 @@ def __init__( self._default_auth = auth self._version = version self._json_serialize = json_serialize + self._json_deserialize = json_deserialize self._raise_for_status = raise_for_status self._auto_decompress = auto_decompress self._trust_env = trust_env @@ -402,7 +412,8 @@ def request( method: str, url: StrOrURL, **kwargs: Unpack[_RequestOptions], - ) -> "_RequestContextManager": ... + ) -> "_RequestContextManager": + ... else: @@ -1159,43 +1170,50 @@ def get( self, url: StrOrURL, **kwargs: Unpack[_RequestOptions], - ) -> "_RequestContextManager": ... + ) -> "_RequestContextManager": + ... def options( self, url: StrOrURL, **kwargs: Unpack[_RequestOptions], - ) -> "_RequestContextManager": ... + ) -> "_RequestContextManager": + ... def head( self, url: StrOrURL, **kwargs: Unpack[_RequestOptions], - ) -> "_RequestContextManager": ... + ) -> "_RequestContextManager": + ... def post( self, url: StrOrURL, **kwargs: Unpack[_RequestOptions], - ) -> "_RequestContextManager": ... + ) -> "_RequestContextManager": + ... def put( self, url: StrOrURL, **kwargs: Unpack[_RequestOptions], - ) -> "_RequestContextManager": ... + ) -> "_RequestContextManager": + ... def patch( self, url: StrOrURL, **kwargs: Unpack[_RequestOptions], - ) -> "_RequestContextManager": ... + ) -> "_RequestContextManager": + ... def delete( self, url: StrOrURL, **kwargs: Unpack[_RequestOptions], - ) -> "_RequestContextManager": ... + ) -> "_RequestContextManager": + ... else: @@ -1322,6 +1340,11 @@ def json_serialize(self) -> JSONEncoder: """Json serializer callable""" return self._json_serialize + @property + def json_deserialize(self) -> JSONDecoder: + """Json deserializer callable""" + return self._json_deserialize + @property def connector_owner(self) -> bool: """Should connector be closed on session closing""" @@ -1453,7 +1476,8 @@ def request( version: HttpVersion = http.HttpVersion11, connector: BaseConnector | None = None, **kwargs: Unpack[_RequestOptions], - ) -> _SessionRequestContextManager: ... + ) -> _SessionRequestContextManager: + ... else: diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index 551b3374c6a..0ec9af44f54 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -107,7 +107,6 @@ class _RequestInfo(NamedTuple): class RequestInfo(_RequestInfo): - def __new__( cls, url: URL, @@ -641,7 +640,7 @@ async def json( self, *, encoding: str | None = None, - loads: JSONDecoder = DEFAULT_JSON_DECODER, + loads: JSONDecoder | None = None, content_type: str | None = "application/json", ) -> Any: """Read and decodes JSON response.""" @@ -663,6 +662,14 @@ async def json( if encoding is None: encoding = self.get_encoding() + # Use session's deserializer if loads not explicitly provided + if loads is None: + loads = ( + self._session.json_deserialize + if self._session is not None + else DEFAULT_JSON_DECODER + ) + return loads(self._body.decode(encoding)) # type: ignore[union-attr] async def __aenter__(self) -> "ClientResponse": From 74e0a9f9eab29c257501d429c95ec0efdfcfe648 Mon Sep 17 00:00:00 2001 From: Jason Tung Date: Mon, 3 Nov 2025 17:34:02 -0500 Subject: [PATCH 2/4] black formatting and changelog entry --- CHANGES/0000.feature.rst | 20 ++++++++++++++++++++ aiohttp/client.py | 27 +++++++++------------------ 2 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 CHANGES/0000.feature.rst diff --git a/CHANGES/0000.feature.rst b/CHANGES/0000.feature.rst new file mode 100644 index 00000000000..88ac447248f --- /dev/null +++ b/CHANGES/0000.feature.rst @@ -0,0 +1,20 @@ +Added ``json_deserialize`` parameter to :class:`~aiohttp.ClientSession` to configure JSON deserialization at the session level, matching the existing ``json_serialize`` parameter -- by :user:`jtung`. + +This allows users to configure a custom JSON deserializer (e.g., ``orjson.loads``) once at session creation, and all calls to :meth:`~aiohttp.ClientResponse.json` will use it by default. + +Example usage: + +.. code-block:: python + + import aiohttp + import orjson + + session = aiohttp.ClientSession( + json_serialize=lambda obj: orjson.dumps(obj).decode(), + json_deserialize=orjson.loads + ) + + async with session.get(url) as response: + data = await response.json() # Uses orjson.loads + +Additionally, fixed an inconsistency where ``json_serialize`` parameter defaulted to ``json.dumps`` directly instead of using ``DEFAULT_JSON_ENCODER`` constant, bringing it in line with other parts of the codebase. diff --git a/aiohttp/client.py b/aiohttp/client.py index 8ac52b7f127..e21fb619329 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -412,8 +412,7 @@ def request( method: str, url: StrOrURL, **kwargs: Unpack[_RequestOptions], - ) -> "_RequestContextManager": - ... + ) -> "_RequestContextManager": ... else: @@ -1170,50 +1169,43 @@ def get( self, url: StrOrURL, **kwargs: Unpack[_RequestOptions], - ) -> "_RequestContextManager": - ... + ) -> "_RequestContextManager": ... def options( self, url: StrOrURL, **kwargs: Unpack[_RequestOptions], - ) -> "_RequestContextManager": - ... + ) -> "_RequestContextManager": ... def head( self, url: StrOrURL, **kwargs: Unpack[_RequestOptions], - ) -> "_RequestContextManager": - ... + ) -> "_RequestContextManager": ... def post( self, url: StrOrURL, **kwargs: Unpack[_RequestOptions], - ) -> "_RequestContextManager": - ... + ) -> "_RequestContextManager": ... def put( self, url: StrOrURL, **kwargs: Unpack[_RequestOptions], - ) -> "_RequestContextManager": - ... + ) -> "_RequestContextManager": ... def patch( self, url: StrOrURL, **kwargs: Unpack[_RequestOptions], - ) -> "_RequestContextManager": - ... + ) -> "_RequestContextManager": ... def delete( self, url: StrOrURL, **kwargs: Unpack[_RequestOptions], - ) -> "_RequestContextManager": - ... + ) -> "_RequestContextManager": ... else: @@ -1476,8 +1468,7 @@ def request( version: HttpVersion = http.HttpVersion11, connector: BaseConnector | None = None, **kwargs: Unpack[_RequestOptions], - ) -> _SessionRequestContextManager: - ... + ) -> _SessionRequestContextManager: ... else: From abbfa7abfde846106ae16cc53514389883a2dd50 Mon Sep 17 00:00:00 2001 From: Jason Tung Date: Mon, 3 Nov 2025 18:14:28 -0500 Subject: [PATCH 3/4] Update changelog with PR number #11741 --- CHANGES/{0000.feature.rst => 11741.feature.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename CHANGES/{0000.feature.rst => 11741.feature.rst} (100%) diff --git a/CHANGES/0000.feature.rst b/CHANGES/11741.feature.rst similarity index 100% rename from CHANGES/0000.feature.rst rename to CHANGES/11741.feature.rst From eac0fef4ef199ceffb17a517c7219b6a762e2607 Mon Sep 17 00:00:00 2001 From: Jason Tung Date: Mon, 3 Nov 2025 18:19:00 -0500 Subject: [PATCH 4/4] Add deserialization and deserializer to spelling wordlist --- docs/spelling_wordlist.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 74ace02c5ec..291274c63a5 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -97,6 +97,8 @@ cythonized de deduplicate defs +deserialization +deserializer Dependabot deprecations DER