Skip to content

Conversation

@kevinpark1217
Copy link

Update JSONEncoder type to accept callables that return either str or bytes.
This allows custom JSON serializers that directly produce bytes to be used
without unnecessary decode/encode cycles.

Changes

  • Updated JSONEncoder type in typedefs.py to Callable[[Any], str | bytes]
  • Modified JsonPayload to handle both str and bytes from dumps()
  • Updated json_response to route bytes to body parameter, str to text
  • Updated WebSocketResponse.send_json to call send_bytes for bytes, send_str for str
  • Updated ClientWebSocketResponse.send_json with same logic

Testing

  • All modified files compile successfully
  • Code formatting validated with black
  • Import ordering validated with isort
  • Functional tests verified both str-returning and bytes-returning encoders work correctly

Backward Compatibility

This maintains full backward compatibility with existing code using json.dumps
while enabling more efficient custom serializers.

Fixes #11988

Update JSONEncoder type to accept callables that return either str or bytes.
This allows custom JSON serializers that directly produce bytes to be used
without unnecessary decode/encode cycles.

Changes:
- Updated JSONEncoder type in typedefs.py to Callable[[Any], str | bytes]
- Modified JsonPayload to handle both str and bytes from dumps()
- Updated json_response to route bytes to body parameter, str to text
- Updated WebSocketResponse.send_json to call send_bytes for bytes, send_str for str
- Updated ClientWebSocketResponse.send_json with same logic

This maintains full backward compatibility with existing code using json.dumps
while enabling more efficient custom serializers.

Fixes aio-libs#11988
@psf-chronographer psf-chronographer bot added the bot:chronographer:provided There is a change note present in this PR label Jan 23, 2026
@codspeed-hq
Copy link

codspeed-hq bot commented Jan 23, 2026

Merging this PR will not alter performance

✅ 59 untouched benchmarks


Comparing kevinpark1217:allow-jsonencoder-return-bytes (d221fc0) with master (95674df)1

Open in CodSpeed

Footnotes

  1. No successful run was found on master (7b4db6f) during the generation of this report, so 95674df was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@codecov
Copy link

codecov bot commented Jan 23, 2026

Codecov Report

❌ Patch coverage is 56.25000% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.74%. Comparing base (95674df) to head (d221fc0).
⚠️ Report is 1 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
aiohttp/client_ws.py 50.00% 1 Missing and 1 partial ⚠️
aiohttp/web_response.py 50.00% 1 Missing and 1 partial ⚠️
aiohttp/web_ws.py 50.00% 1 Missing and 1 partial ⚠️
aiohttp/payload.py 66.66% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #11989      +/-   ##
==========================================
- Coverage   98.75%   98.74%   -0.01%     
==========================================
  Files         127      127              
  Lines       44655    44667      +12     
  Branches     2367     2371       +4     
==========================================
+ Hits        44098    44106       +8     
  Misses        396      396              
- Partials      161      165       +4     
Flag Coverage Δ
CI-GHA 98.60% <56.25%> (-0.01%) ⬇️
OS-Linux 98.34% <56.25%> (-0.01%) ⬇️
OS-Windows 96.69% <56.25%> (-0.02%) ⬇️
OS-macOS 97.57% <56.25%> (-0.03%) ⬇️
Py-3.10.11 97.12% <56.25%> (-0.02%) ⬇️
Py-3.10.19 97.60% <56.25%> (-0.02%) ⬇️
Py-3.11.14 97.81% <56.25%> (-0.02%) ⬇️
Py-3.11.9 97.33% <56.25%> (-0.02%) ⬇️
Py-3.12.10 97.42% <56.25%> (-0.02%) ⬇️
Py-3.12.12 97.90% <56.25%> (-0.02%) ⬇️
Py-3.13.11 98.15% <56.25%> (-0.02%) ⬇️
Py-3.14.2 98.13% <56.25%> (-0.02%) ⬇️
Py-3.14.2t 97.22% <56.25%> (-0.01%) ⬇️
Py-pypy3.11.13-7.3.20 97.37% <56.25%> (-0.02%) ⬇️
VM-macos 97.57% <56.25%> (-0.03%) ⬇️
VM-ubuntu 98.34% <56.25%> (-0.01%) ⬇️
VM-windows 96.69% <56.25%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@kevinpark1217 kevinpark1217 changed the title Allow JSONEncoder to return bytes directly Allow JSONEncoder to return bytes directly Jan 23, 2026
@Dreamsorcerer
Copy link
Member

@bdraco Do the benchmarks already cover these cases?

@webknjaz
Copy link
Member

IIRC, there was a similar request years ago, rejected.

@webknjaz
Copy link
Member

I think it was #4482

Copy link
Member

@webknjaz webknjaz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No tests? How do we know this keeps working?

dumps: JSONEncoder = DEFAULT_JSON_ENCODER,
) -> None:
await self.send_str(dumps(data), compress=compress)
result = dumps(data)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we plz not use meaningless var names?

@@ -0,0 +1,4 @@
Updated the ``JSONEncoder`` type to accept callables returning either ``str`` or ``bytes``.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Updated the ``JSONEncoder`` type to accept callables returning either ``str`` or ``bytes``.
Updated the ``JSONEncoder`` type to accept callables returning either :data:`str` or :data:`bytes`.

@@ -0,0 +1,4 @@
Updated the ``JSONEncoder`` type to accept callables returning either ``str`` or ``bytes``.
This allows a custom JSON serializer that directly produces bytes to be used with
``ClientSession.json_serialize``, ``JsonPayload``, ``json_response``, and websocket
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should probably be properly linked using appropriate RST roles.

@Dreamsorcerer
Copy link
Member

IIRC, there was a similar request years ago, rejected.

With ujson dead, I think we're ready to change this: #10795 (comment)

My only question is whether the isinstance() calls here are fine, or we should add a new parameter and avoid the isinstance() checks. I suspect this is one of the performance hot paths for some cases (like websockets on homeassistant?).

@webknjaz
Copy link
Member

webknjaz commented Jan 25, 2026

Ah, fair. I also had a feeling that I'd prefer having a new API rather than overloading the existing one..

@bdraco
Copy link
Member

bdraco commented Jan 25, 2026

Thanks for working on this! The use case makes sense.

I agree with webknjaz about preferring a new API over isinstance(). A few concerns:

  1. isinstance() in the hot path. Minor, but runtime overhead on every call when the encoder type is fixed for the session lifetime.

  2. WebSocket frame type changes silently. With a bytes encoder, send_json() now calls send_bytes() (binary frame) instead of send_str() (text frame). JSON is text, so this could break clients expectng text frames.

  3. Harder to reason about. Union return types that branch at runtime are messier long-term.

Alternative: Add explicit parallel methods like JsonBytesPayload, json_bytes_response(), send_json_bytes(), and json_serialize_bytes param on ClientSession. No isinstance checks, clear contracts, existing code untouched.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided There is a change note present in this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow dumps: JSONEncoder callable to directly return bytes

4 participants