Skip to content

[Feature] First-class support for pre-serialized JSON response payloads #1648

Description

@WonderLawrence

[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:

  1. serve cached, already-serialized results (response caches keyed by block hash), and
  2. 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):

  1. A RawJson(String) newtype in jsonrpsee-types that implements IntoResponse, so #[rpc] methods can declare -> RpcResult<RawJson>;
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions