Skip to content

Allow JSONEncoder to return bytes directly#11989

Merged
Dreamsorcerer merged 6 commits intoaio-libs:masterfrom
kevinpark1217:allow-jsonencoder-return-bytes
Feb 21, 2026
Merged

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

Conversation

@kevinpark1217
Copy link
Copy Markdown
Contributor

@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
Copy Markdown

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
Copy Markdown

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).
⚠️ Report is 17 commits behind head on master.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #11989      +/-   ##
==========================================
- Coverage   98.77%   98.77%   -0.01%     
==========================================
  Files         128      128              
  Lines       44881    45006     +125     
  Branches     2382     2385       +3     
==========================================
+ Hits        44332    44453     +121     
- Misses        390      393       +3     
- Partials      159      160       +1     
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
Copy Markdown
Member

@bdraco Do the benchmarks already cover these cases?

@webknjaz
Copy link
Copy Markdown
Member

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

@webknjaz
Copy link
Copy Markdown
Member

I think it was #4482

Copy link
Copy Markdown
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?

Comment thread aiohttp/client_ws.py Outdated
Comment thread CHANGES/11989.feature.rst Outdated
Comment thread CHANGES/11989.feature.rst Outdated
@Dreamsorcerer
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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 #11988
@kevinpark1217 kevinpark1217 force-pushed the allow-jsonencoder-return-bytes branch from d6e60c5 to 584c004 Compare February 2, 2026 18:38
@kevinpark1217
Copy link
Copy Markdown
Contributor 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?

Comment thread CHANGES/11989.feature.rst
Comment thread aiohttp/typedefs.py Outdated
Comment thread aiohttp/payload.py Outdated
Comment thread aiohttp/payload.py
Comment thread aiohttp/web_response.py Outdated
Comment thread aiohttp/web_response.py Outdated
Comment thread aiohttp/web_ws.py Outdated
Comment thread aiohttp/web_ws.py Outdated
Comment thread tests/test_web_websocket.py
@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
Comment thread aiohttp/client_ws.py
Copy link
Copy Markdown

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.

Comment thread aiohttp/web_response.py Outdated
Comment thread aiohttp/web_ws.py Outdated
Comment thread aiohttp/client_ws.py Outdated
- 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
Comment thread docs/web_reference.rst
Comment thread docs/web_reference.rst Outdated
Comment thread tests/test_web_response.py Outdated
Comment thread aiohttp/typedefs.py
…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
Copy Markdown
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
Copy Markdown
Contributor Author

I think this looks reasonable to me.

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

Comment thread CHANGES/11989.feature.rst
@Dreamsorcerer Dreamsorcerer merged commit 67fa1f5 into aio-libs:master Feb 21, 2026
41 of 42 checks passed
@patchback
Copy link
Copy Markdown
Contributor

patchback Bot commented Feb 21, 2026

Backport to 3.14: 💔 cherry-picking failed — conflicts found

❌ Failed to cleanly apply 67fa1f5 on top of patchback/backports/3.14/67fa1f5e64a95a5e081f566c4718bc7062c7dbdb/pr-11989

Backporting merged PR #11989 into master

  1. Ensure you have a local repo clone of your fork. Unless you cloned it
    from the upstream, this would be your origin remote.
  2. Make sure you have an upstream repo added as a remote too. In these
    instructions you'll refer to it by the name upstream. If you don't
    have it, here's how you can add it:
    $ git remote add upstream https://github.com/aio-libs/aiohttp.git
  3. Ensure you have the latest copy of upstream and prepare a branch
    that will hold the backported code:
    $ git fetch upstream
    $ git checkout -b patchback/backports/3.14/67fa1f5e64a95a5e081f566c4718bc7062c7dbdb/pr-11989 upstream/3.14
  4. Now, cherry-pick PR Allow JSONEncoder to return bytes directly #11989 contents into that branch:
    $ git cherry-pick -x 67fa1f5e64a95a5e081f566c4718bc7062c7dbdb
    If it'll yell at you with something like fatal: Commit 67fa1f5e64a95a5e081f566c4718bc7062c7dbdb is a merge but no -m option was given., add -m 1 as follows instead:
    $ git cherry-pick -m1 -x 67fa1f5e64a95a5e081f566c4718bc7062c7dbdb
  5. At this point, you'll probably encounter some merge conflicts. You must
    resolve them in to preserve the patch from PR Allow JSONEncoder to return bytes directly #11989 as close to the
    original as possible.
  6. Push this branch to your fork on GitHub:
    $ git push origin patchback/backports/3.14/67fa1f5e64a95a5e081f566c4718bc7062c7dbdb/pr-11989
  7. Create a PR, ensure that the CI is green. If it's not — update it so that
    the tests and any other checks pass. This is it!
    Now relax and wait for the maintainers to process your pull request
    when they have some cycles to do reviews. Don't worry — they'll tell you if
    any improvements are necessary when the time comes!

🤖 @patchback
I'm built with octomachinery and
my source is open — https://github.com/sanitizers/patchback-github-app.

Dreamsorcerer pushed a commit that referenced this pull request Feb 21, 2026
Dreamsorcerer added a commit that referenced this pull request Feb 22, 2026
(cherry picked from commit 67fa1f5)

---------

Co-authored-by: Kevin Park <kevin.park1217@gmail.com>
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

5 participants