Skip to content

Allow JSONEncoder to return bytes directly#11989

Open
kevinpark1217 wants to merge 6 commits intoaio-libs:masterfrom
kevinpark1217:allow-jsonencoder-return-bytes
Open

Allow JSONEncoder to return bytes directly#11989
kevinpark1217 wants to merge 6 commits intoaio-libs:masterfrom
kevinpark1217:allow-jsonencoder-return-bytes

Conversation

@kevinpark1217
Copy link

@kevinpark1217 kevinpark1217 commented Jan 23, 2026

Summary

Add explicit APIs for bytes-returning JSON serializers (like orjson), addressing maintainer feedback to avoid isinstance() checks in hot paths.

Changes

  1. Kept JSONEncoder unchanged - still returns str only
  2. Added new JSONBytesEncoder type - Callable[[Any], bytes]
  3. No default bytes encoder - users must explicitly provide their encoder (e.g., orjson.dumps)
  4. No isinstance() checks in hot paths - separate explicit APIs instead
  5. Uses object instead of Any for data parameters in new APIs
  6. dumps is keyword-only in send_json_bytes() methods, matching send_json() signature

New APIs

  • JSONBytesEncoder type in typedefs.py
  • JsonBytesPayload class - requires dumps parameter
  • json_bytes_response() function - requires dumps parameter
  • send_json_bytes() methods on WebSocketResponse and ClientWebSocketResponse - sends as binary frames, requires dumps keyword argument
  • ClientSession(json_serialize_bytes=...) parameter - None by default, falls back to json_serialize if not set

Documentation

  • Added Sphinx doc entries for json_bytes_response(), WebSocketResponse.send_json_bytes(), and ClientWebSocketResponse.send_json_bytes()
  • Changelog uses proper RST cross-reference roles

Benefits

  • Avoids runtime overhead of isinstance() checks
  • Makes WebSocket frame type explicit (binary for bytes)
  • Provides clear API separation for different use cases
  • No behavioral changes to existing code

Closes #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 (4972fcb) with master (a640f4f)

Open in CodSpeed

@codecov
Copy link

codecov bot commented Jan 23, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.77%. Comparing base (a640f4f) to head (4972fcb).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff            @@
##           master   #11989    +/-   ##
========================================
  Coverage   98.76%   98.77%            
========================================
  Files         128      128            
  Lines       44881    45006   +125     
  Branches     2382     2385     +3     
========================================
+ Hits        44328    44453   +125     
  Misses        393      393            
  Partials      160      160            
Flag Coverage Δ
CI-GHA 98.62% <100.00%> (+<0.01%) ⬆️
OS-Linux 98.36% <100.00%> (+<0.01%) ⬆️
OS-Windows 96.72% <100.00%> (+<0.01%) ⬆️
OS-macOS 97.62% <100.00%> (+0.01%) ⬆️
Py-3.10.11 97.17% <100.00%> (+<0.01%) ⬆️
Py-3.10.19 97.64% <100.00%> (+<0.01%) ⬆️
Py-3.11.14 97.84% <100.00%> (+<0.01%) ⬆️
Py-3.11.9 97.37% <100.00%> (+<0.01%) ⬆️
Py-3.12.10 97.46% <100.00%> (+<0.01%) ⬆️
Py-3.12.12 97.94% <100.00%> (+<0.01%) ⬆️
Py-3.13.12 98.19% <100.00%> (+<0.01%) ⬆️
Py-3.14.3 98.15% <100.00%> (+<0.01%) ⬆️
Py-3.14.3t 97.24% <100.00%> (+<0.01%) ⬆️
Py-pypy3.11.13-7.3.20 97.39% <100.00%> (+<0.01%) ⬆️
VM-macos 97.62% <100.00%> (+0.01%) ⬆️
VM-ubuntu 98.36% <100.00%> (+<0.01%) ⬆️
VM-windows 96.72% <100.00%> (+<0.01%) ⬆️

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?

@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.

@kevinpark1217 kevinpark1217 force-pushed the allow-jsonencoder-return-bytes branch 6 times, most recently from 5360cba to c515669 Compare February 2, 2026 17:49
@kevinpark1217 kevinpark1217 force-pushed the allow-jsonencoder-return-bytes branch from c515669 to cae039d Compare February 2, 2026 18:29
This implements the maintainers' suggested approach of creating
explicit parallel APIs instead of using isinstance() checks:

- Add JSONEncoderBytes type (no default encoder provided)
- Add JsonBytesPayload class with required dumps parameter
- Add json_bytes_response() function with required dumps parameter
- Add send_json_bytes() methods to WebSocketResponse and
  ClientWebSocketResponse (sends as binary frames, required dumps)
- Add json_serialize_bytes parameter to ClientSession (None by default,
  falls back to json_serialize if not set)
- Export new APIs from aiohttp.web

This avoids isinstance() overhead in hot paths and provides clear
semantics for WebSocket frame types. Users must explicitly provide
their bytes-returning encoder.

Closes aio-libs#11988
@kevinpark1217 kevinpark1217 force-pushed the allow-jsonencoder-return-bytes branch from d6e60c5 to 584c004 Compare February 2, 2026 18:38
@kevinpark1217
Copy link
Author

@webknjaz @bdraco I have updated the PR. Now it's all new parallel API with bytes serialization option, rather than having isinstance() in the hot-path. Can you guys take a look again?

@webknjaz webknjaz requested review from Dreamsorcerer and bdraco and removed request for asvetlov February 3, 2026 08:41
@bdraco bdraco added the backport-3.14 Trigger automatic backporting to the 3.14 release branch by Patchback robot label Feb 3, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds explicit, bytes-oriented JSON serialization APIs (for serializers like orjson) so callers can avoid strbytes conversion overhead and keep hot paths free of runtime isinstance() checks.

Changes:

  • Introduces JSONEncoderBytes and new bytes-specific primitives: JsonBytesPayload and web.json_bytes_response().
  • Adds send_json_bytes() to server/client WebSocket responses to transmit JSON as binary frames.
  • Adds ClientSession(json_serialize_bytes=...) to send request JSON bodies using a bytes-returning encoder, plus corresponding test coverage.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
aiohttp/typedefs.py Adds JSONEncoderBytes callable type alias.
aiohttp/payload.py Adds JsonBytesPayload for bytes-returning JSON dumps.
aiohttp/client.py Adds json_serialize_bytes session option and uses JsonBytesPayload when set.
aiohttp/web_response.py Adds json_bytes_response() and exports it.
aiohttp/web.py Re-exports json_bytes_response() from aiohttp.web.
aiohttp/web_ws.py Adds WebSocketResponse.send_json_bytes() for binary JSON frames.
aiohttp/client_ws.py Adds ClientWebSocketResponse.send_json_bytes() for binary JSON frames.
tests/test_payload.py Adds unit tests for JsonBytesPayload.
tests/test_web_response.py Adds tests for web.json_bytes_response().
tests/test_web_websocket.py Adds error-path tests for WebSocketResponse.send_json_bytes().
tests/test_client_ws_functional.py Adds functional tests asserting send_json_bytes() uses binary frames.
tests/test_client_functional.py Adds functional test for ClientSession(json_serialize_bytes=...).
CHANGES/11989.feature.rst Adds a towncrier feature fragment documenting the new APIs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Rename JSONEncoderBytes to JSONBytesEncoder for consistency
- Use object instead of Any in new API signatures
- Make dumps keyword-only in send_json_bytes to match send_json pattern
- Fix body check to use `is not None` in json_bytes_response
- Alphabetize import ordering
@kevinpark1217 kevinpark1217 force-pushed the allow-jsonencoder-return-bytes branch from 8238796 to 90af396 Compare February 6, 2026 07:58
@kevinpark1217 kevinpark1217 force-pushed the allow-jsonencoder-return-bytes branch from bacad25 to e91411e Compare February 6, 2026 08:23
- Add Sphinx doc entries for json_bytes_response(), send_json_bytes()
  in both WebSocketResponse and ClientWebSocketResponse
- Use proper RST cross-reference roles in changelog
- Close ws at end of test_send_json_bytes_nonjson
- Add orjson to docs spelling wordlist
@kevinpark1217 kevinpark1217 force-pushed the allow-jsonencoder-return-bytes branch from e91411e to f56a472 Compare February 6, 2026 08:41
…tic test values

- Revert parameter types from `object` back to `Any` per Dreamsorcerer's
  guidance that `object` is inappropriate for JSON serializer inputs
- Use :class:`str` and :class:`bytes` RST roles in docs
- Use static byte values in test_passing_body_only instead of json.dumps()
Copy link
Member

@Dreamsorcerer Dreamsorcerer left a comment

Choose a reason for hiding this comment

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

I think this looks reasonable to me.

@kevinpark1217
Copy link
Author

I think this looks reasonable to me.

@Dreamsorcerer Thanks! It's ready to be merged at you convenience

@@ -0,0 +1,7 @@
Added explicit APIs for bytes-returning JSON serializer:
``JSONBytesEncoder`` type, ``JsonBytesPayload``,
Copy link
Member

Choose a reason for hiding this comment

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

would be nice to make these real rst links

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

Labels

backport-3.14 Trigger automatic backporting to the 3.14 release branch by Patchback robot 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

Comments