[Feature] First-class support for pre-serialized JSON response payloads
Motivation
Servers that produce large responses (Ethereum execution clients — reth and its forks — routinely return multi-MB eth_getLogs / debug_traceBlock* / call-bundle results) increasingly want to:
- serve cached, already-serialized results (response caches keyed by block hash), and
- serialize huge results with SIMD-accelerated encoders (e.g. sonic-rs) instead of serde_json.
Today the only way to hand jsonrpsee a pre-serialized payload is serde_json::value::RawValue. That works — the envelope writer embeds it verbatim — but constructing it via RawValue::from_string re-parses the entire string to validate it, which claws back much of the win. Measured on x86_64/AVX2 for a 4 MB call-bundle-shaped result:
| path |
time per response |
| serde_json serialize (status-quo generic path) |
2842 µs |
| sonic-rs serialize |
272 µs |
sonic-rs serialize + RawValue::from_string validation |
1011 µs |
Relatedly, jsonrpsee itself pays this validation cost on every response: MethodResponse::response serializes with serde_json and then immediately revalidates its own output via RawValue::from_string(result).expect("Valid JSON String; qed") (core/src/server/method_response.rs:194; same pattern for batches at line 373). The output of serde_json::to_writer is valid by construction, so this is a redundant O(n) parse on the hot path for all users.
Proposal
Add a way to return a pre-serialized JSON payload that is spliced into the response envelope by string concatenation, bypassing serde entirely. The caller asserts validity — the same contract RawValue already has, minus the forced re-validation. Possible shape (open to alternatives):
- A
RawJson(String) newtype in jsonrpsee-types that implements IntoResponse, so #[rpc] methods can declare -> RpcResult<RawJson>;
ResponsePayload::raw_json(impl Into<String>) (new variant) and/or MethodResponse::raw_response(id, json, max_response_size) for manually registered methods and middleware.
max_response_size can be enforced directly on the string length before splicing. Error responses keep the existing typed path. Adding a ResponsePayload variant is semver-breaking, but master is in the 0.26 breaking cycle anyway.
As an independent follow-up in the same direction: storing the serialized response internally as String instead of Box<RawValue> would remove the self-revalidation at method_response.rs:194/:373 for every user (as_json() returning &str instead of &RawValue).
Context
We (MegaETH, a reth fork) currently run the RawValue-based workaround in production (RPC methods returning Box<RawValue> serialized with sonic-rs). It nets 1.8–2.8× on large responses, but roughly a third of the theoretical gain is spent on the redundant validation parse described above. We're happy to contribute the PR if the design direction is acceptable.
[Feature] First-class support for pre-serialized JSON response payloads
Motivation
Servers that produce large responses (Ethereum execution clients — reth and its forks — routinely return multi-MB
eth_getLogs/debug_traceBlock*/ call-bundle results) increasingly want to:Today the only way to hand jsonrpsee a pre-serialized payload is
serde_json::value::RawValue. That works — the envelope writer embeds it verbatim — but constructing it viaRawValue::from_stringre-parses the entire string to validate it, which claws back much of the win. Measured on x86_64/AVX2 for a 4 MB call-bundle-shaped result:RawValue::from_stringvalidationRelatedly, jsonrpsee itself pays this validation cost on every response:
MethodResponse::responseserializes with serde_json and then immediately revalidates its own output viaRawValue::from_string(result).expect("Valid JSON String; qed")(core/src/server/method_response.rs:194; same pattern for batches at line 373). The output ofserde_json::to_writeris valid by construction, so this is a redundant O(n) parse on the hot path for all users.Proposal
Add a way to return a pre-serialized JSON payload that is spliced into the response envelope by string concatenation, bypassing serde entirely. The caller asserts validity — the same contract
RawValuealready has, minus the forced re-validation. Possible shape (open to alternatives):RawJson(String)newtype injsonrpsee-typesthat implementsIntoResponse, so#[rpc]methods can declare-> RpcResult<RawJson>;ResponsePayload::raw_json(impl Into<String>)(new variant) and/orMethodResponse::raw_response(id, json, max_response_size)for manually registered methods and middleware.max_response_sizecan be enforced directly on the string length before splicing. Error responses keep the existing typed path. Adding aResponsePayloadvariant is semver-breaking, but master is in the 0.26 breaking cycle anyway.As an independent follow-up in the same direction: storing the serialized response internally as
Stringinstead ofBox<RawValue>would remove the self-revalidation atmethod_response.rs:194/:373for every user (as_json()returning&strinstead of&RawValue).Context
We (MegaETH, a reth fork) currently run the
RawValue-based workaround in production (RPC methods returningBox<RawValue>serialized with sonic-rs). It nets 1.8–2.8× on large responses, but roughly a third of the theoretical gain is spent on the redundant validation parse described above. We're happy to contribute the PR if the design direction is acceptable.