Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 12, 2026

Optimize JsonRpcMessage.Converter.Read method

Based on PR #639 by @Scooletz and reviewer feedback from @eiriktsarpalis and @stephentoub, implementing single-pass deserialization optimization.

Attribution

This work is based on the original implementation by @Scooletz (Szymon Kulec) in PR #639, which demonstrated ~50% performance improvement through single-pass deserialization.

Complete Implementation:

  • Explore repository structure and understand current implementation
  • Verify build succeeds with current code
  • Implement single-pass parsing directly in Read method
  • Build and verify no compilation errors
  • Run tests to ensure no regressions (1035 tests passed)
  • Write specific tests for the optimization (16 new tests added)
  • Address all code review feedback:
    • ✅ Use while(true) pattern with Debug.Assert for validation
    • ✅ Add proper attribution to original author @Scooletz
    • ✅ Optimize "jsonrpc" validation with raw byte comparison (no string allocation)
    • ✅ Inline all parsing logic directly into Read method
    • ✅ Use local variables instead of intermediate struct
    • ✅ Restructure control flow with nested if statements for clarity
    • ✅ Rename @params to parameters (no escape character needed)
    • ✅ Consolidate variable declarations
    • ✅ Improve error messages
    • ✅ Remove redundant JsonRpc property assignments (uses default value)
    • ✅ Delete JsonRpcVersion constant
    • ✅ Prioritize error check before result check in responses
  • Final testing complete

Performance Benefits (Expected)

Based on @Scooletz's benchmarks in PR #639:

  • ~50% faster deserialization (from ~1μs to ~500ns)
  • ~50% reduction in allocations
  • Additional benefits:
    • No intermediate Union struct
    • No string allocation for "jsonrpc" validation (uses ValueTextEquals("2.0"u8))
    • No redundant property assignments (JsonRpc defaults to "2.0")
    • Simpler, more maintainable code with all logic in one place
    • Clear control flow with nested if statements

Changes Made

Single-pass parsing directly in Read() method

  • Replaced double deserialization (JsonDocument.ParseValue + JsonSerializer.Deserialize) with manual single-pass reading
  • Manual JSON reading with while(true) pattern
  • Debug.Assert statements for validation (converters get fully buffered objects)
  • Uses options.GetTypeInfo<T>() for AOT compatibility
  • Skips unknown properties for forward compatibility
  • Validates "jsonrpc" using reader.ValueTextEquals("2.0"u8) - no string allocation
  • All parsing state in local variables (no intermediate struct)
  • Direct object construction from parsed local variables
  • Eliminates second deserialization pass entirely
  • Uses id.Id is not null to check for ID presence (leverages RequestId's nullable Id property)
  • Nested if statements for clear message type determination
  • Error responses take precedence over success responses
  • HasResult flag tracks whether "result" property was present (needed since null is a valid result value per JSON-RPC 2.0)
  • Relies on JsonRpc property default value instead of explicit assignment

Testing

All existing tests pass (1035 tests) plus 16 new comprehensive tests covering all message types and edge cases.

Credits

Original prompt

Background

This PR is based on the work from PR #639 by @Scooletz, which optimizes JsonRpcMessage.Converter.Read by replacing the double deserialization pattern with a single-pass approach. The original implementation showed ~50% faster deserialization and roughly halved allocations.

Changes Required

Optimize the JsonRpcMessage.Converter.Read method in src/ModelContextProtocol.Core/Protocol/JsonRpcMessage.cs with the following approach based on reviewer feedback from @eiriktsarpalis:

1. Add a private Union struct inside the Converter class

The struct should have private fields for all the properties needed to construct the various JsonRpcMessage subtypes:

  • JsonRpc (string) - the jsonrpc version
  • Id (RequestId) - the message id
  • Method (string?) - the method name
  • Params (JsonNode?) - the parameters
  • Error (JsonRpcErrorDetail?) - the error details
  • Result (JsonNode?) - the result

2. Hand-craft a static Parse(ref Utf8JsonReader reader, JsonSerializerOptions options) method

Instead of using JsonSerializer.Deserialize<Union>(), implement a manual parsing method that:

  • Reads through the JSON object properties one by one
  • Populates the Union struct fields directly
  • Uses the options parameter to deserialize complex types like RequestId, JsonNode, and JsonRpcErrorDetail

This approach:

  • Avoids generating serialization code for the Union type
  • Further improves performance by avoiding the overhead of the source generator
  • Keeps the implementation detail completely private

3. Update the Read method

Replace the current implementation:

using var doc = JsonDocument.ParseValue(ref reader);
var root = doc.RootElement;
// ... property checks and rawText deserialization

With:

var union = Union.Parse(ref reader, options);
// ... use union fields to construct the appropriate message type

4. Construct message types directly

Instead of deserializing from rawText, construct the message types directly from the parsed union fields, similar to Scooletz's approach:

return new JsonRpcRequest
{
    Id = union.Id,
    Method = union.Method,
    JsonRpc = union.JsonRpc,
    Params = union.Params,
};

Performance Expectations

Based on Scooletz's benchmarks, the optimization should provide:

  • ~50% faster deserialization (from ~1μs to ~500ns)
  • ~50% reduction in allocations

Reference

Important Notes

  • The Union struct must be private (not internal or public)
  • Do NOT register the Union type with the JSON source generator - the hand-crafted Parse method eliminates this need
  • Ensure all existing tests pass
  • The current main branch uses JsonRpcMessageContext? Context property instead of separate RelatedTransport and ExecutionContext properties - make sure to work with the current main branch structure

This pull request was created from Copilot chat.


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI changed the title [WIP] Optimize JsonRpcMessage.Converter.Read method Optimize JsonRpcMessage deserialization with single-pass parsing Jan 12, 2026
Copilot AI requested a review from eiriktsarpalis January 12, 2026 16:25
@eiriktsarpalis
Copy link
Member

@copilot please ensure all commits contain appropriate attribution to @Scooletz, the original author.

@eiriktsarpalis eiriktsarpalis marked this pull request as ready for review January 12, 2026 17:14
Copy link
Contributor Author

Copilot AI commented Jan 12, 2026

@copilot please ensure all commits contain appropriate attribution to @Scooletz, the original author.

The existing commits (9f01d2d through 310a8e0) have Co-authored-by: eiriktsarpalis but would need git rebase -i to add Co-authored-by: Scooletz attribution to each commit message. This would require force-pushing the rebased history. Would you like me to proceed with creating a new commit that adds this attribution in the commit message, or would you prefer to handle the rebase manually?

@eiriktsarpalis
Copy link
Member

@copilot please go ahead if possible

Copy link
Contributor Author

Copilot AI commented Jan 12, 2026

@copilot please go ahead if possible

I've updated the PR description to include proper attribution to @Scooletz (the original author of PR #639). However, adding Co-authored-by: Scooletz trailers to the existing commits (9f01d2d through 310a8e0) would require git rebase -i to amend each commit message, followed by a force-push. The available tools don't support modifying existing commit messages - they can only create new commits. If you need the Co-authored-by trailers in the individual commit messages, you'll need to do an interactive rebase manually.

Co-authored-by: scooletz <[email protected]>
Co-authored-by: eiriktsarpalis <[email protected]>
@eiriktsarpalis eiriktsarpalis force-pushed the copilot/optimize-jsonrpcmessage-read-method branch from e606ad3 to 7b8cb42 Compare January 12, 2026 17:32
Copy link
Contributor

@stephentoub stephentoub left a comment

Choose a reason for hiding this comment

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

@eiriktsarpalis, have we validated the expected perf improvements from this?

- Use switch expression on (HasId, HasMethod) tuple to reduce branching
- Replace null check exceptions with Debug.Assert since HasError/HasMethod
  flags are only set when values are non-null
- Simplify control flow for better performance and readability
- All 1035 tests still pass

Co-authored-by: Scooletz <[email protected]>
Co-authored-by: eiriktsarpalis <[email protected]>

Co-authored-by: stephentoub <[email protected]>
Copilot AI requested a review from stephentoub January 12, 2026 23:23
Replaced the HasId boolean flag with a direct null check on Id.Id in
the pattern matching. Since RequestId.Id is nullable (object?), we can
use `Id.Id: not null` to determine if an 'id' property was present in
the JSON. This further reduces the Union struct size from 2 boolean
flags to just 1 (HasResult is still needed since Result can be null).

Note: HasResult cannot be removed because according to JSON-RPC 2.0,
the 'result' property can have a null value, so we need HasResult to
distinguish "no result property" from "result property with null value".

Co-authored-by: Scooletz <[email protected]>
Co-authored-by: eiriktsarpalis <[email protected]>
Co-authored-by: stephentoub <[email protected]>

Co-authored-by: stephentoub <[email protected]>
auto-merge was automatically disabled January 13, 2026 14:45

Head branch was pushed to by a user without write access

Copilot AI requested a review from stephentoub January 13, 2026 14:46
Major refactoring to simplify the code based on reviewer feedback:

1. Inlined ParseUnion method directly into Read() - eliminates method
   call overhead and makes the parsing logic more straightforward

2. Replaced Union struct with local variables in Read() - removes the
   need for an intermediate data structure, reducing memory overhead

3. Optimized "jsonrpc" validation using reader.ValueTextEquals("2.0"u8)
   instead of allocating a string - validates raw UTF-8 bytes directly
   without string allocation

4. Track jsonrpc presence with hasJsonRpc boolean flag, validate after
   parsing completes

5. Replaced switch expression pattern matching with simple if statements
   for clarity since we no longer have a struct

Benefits:
- No Union struct to copy around
- No string allocation for "jsonrpc" property
- Cleaner, more straightforward code
- All the parsing logic in one place
- Still maintains single-pass deserialization

All 1035 tests pass.

Co-authored-by: Scooletz <[email protected]>
Co-authored-by: eiriktsarpalis <[email protected]>
Co-authored-by: stephentoub <[email protected]>

Co-authored-by: stephentoub <[email protected]>
…iables

- Rename @params to parameters to avoid needing @ escape character
- Move hasJsonRpc declaration into main variable declaration block
- Update error message for missing jsonrpc from "Invalid or missing" to "Missing"
- Restructure method/id checks to use nested if statements for clarity
- Restructure response checks (result/error) to use nested if statements
- All 16 JsonRpcMessageConverterTests pass

Co-authored-by: Scooletz <[email protected]>
Co-authored-by: eiriktsarpalis <[email protected]>
Co-authored-by: stephentoub <[email protected]>

Co-authored-by: stephentoub <[email protected]>
@stephentoub
Copy link
Contributor

@copilot

- Delete JsonRpcVersion constant since JsonRpc property defaults to "2.0"
- Remove all JsonRpc property assignments in object initializers (uses default)
- Move error check before hasResult check in response handling (error takes precedence)
- All 16 JsonRpcMessageConverterTests pass

Co-authored-by: Scooletz <[email protected]>
Co-authored-by: eiriktsarpalis <[email protected]>
Co-authored-by: stephentoub <[email protected]>

Co-authored-by: stephentoub <[email protected]>
@stephentoub stephentoub enabled auto-merge (squash) January 13, 2026 16:41
@stephentoub stephentoub merged commit 2bc105b into main Jan 13, 2026
9 of 10 checks passed
@stephentoub stephentoub deleted the copilot/optimize-jsonrpcmessage-read-method branch January 13, 2026 16:55
@stephentoub stephentoub restored the copilot/optimize-jsonrpcmessage-read-method branch January 13, 2026 16:55
@stephentoub stephentoub deleted the copilot/optimize-jsonrpcmessage-read-method branch January 13, 2026 16:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants