Skip to content

fix(dotnet): add AOT-safe RequestId serialization for StreamJsonRpc#783

Open
PureWeen wants to merge 1 commit intogithub:mainfrom
PureWeen:fix/dotnet-requestid-aot-serialization
Open

fix(dotnet): add AOT-safe RequestId serialization for StreamJsonRpc#783
PureWeen wants to merge 1 commit intogithub:mainfrom
PureWeen:fix/dotnet-requestid-aot-serialization

Conversation

@PureWeen
Copy link

Problem

When CancellationToken fires during a JSON-RPC operation, StreamJsonRpc's StandardCancellationStrategy sends a $/cancelRequest notification containing a RequestId. The SDK's AOT serialization setup (source-generated contexts + MakeReadOnly()) has no type info for RequestId, causing a NotSupportedException that silently kills the JSON-RPC connection.

Fixes: PureWeen/PolyPilot#319

Root Cause

Commit 4e7319e locked serialization to source-generated contexts only. RequestId couldn't be added via [JsonSerializable] because it has [JsonConverter(typeof(RequestIdSTJsonConverter))] and that converter is internal to StreamJsonRpc (SYSLIB1220). The converter also can't be used directly (CS0122).

Solution

Add AOT-safe RequestIdTypeInfoResolver + RequestIdJsonConverter:

  • RequestIdJsonConverter — hand-written converter using RequestId's public API (Number, String, constructors). Uses TryGetInt64() with string fallback for overflow/decimal safety.
  • RequestIdTypeInfoResolverIJsonTypeInfoResolver that returns type info via JsonMetadataServices.CreateValueInfo<T>() (the same API the source generator uses internally). Zero reflection.

This follows the existing converter pattern in the SDK (see PermissionRequestResultKind.Converter in Types.cs).

Tests

  • RequestId_CanBeSerializedAndDeserialized_WithSdkOptions — round-trips long, string, and null RequestIds
  • SerializerOptions_CanResolveRequestIdTypeInfo — verifies the resolver chain produces type info
  • RequestId_NumericEdgeCases_RoundTrip[Theory] covering 0, -1, long.MaxValue

All 5 tests fail without the fix (NotSupportedException) and pass with it.

@PureWeen PureWeen requested a review from a team as a code owner March 10, 2026 20:54
Copilot AI review requested due to automatic review settings March 10, 2026 20:54
Copy link
Contributor

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

This PR fixes a NativeAOT/System.Text.Json source-generation gap in the .NET SDK that can crash the StreamJsonRpc connection when CancellationToken triggers and $/cancelRequest must serialize a StreamJsonRpc.RequestId.

Changes:

  • Extend the SDK’s JsonSerializerOptions configuration with an AOT-safe IJsonTypeInfoResolver + custom JsonConverter<RequestId>.
  • Add a new test suite to validate RequestId round-tripping and type-info resolution under the SDK’s configured serializer options.

Reviewed changes

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

File Description
dotnet/src/Client.cs Adds RequestIdTypeInfoResolver + RequestIdJsonConverter and wires it into the serializer options resolver chain before MakeReadOnly().
dotnet/test/SerializationTests.cs Adds new unit tests verifying RequestId serialization/deserialization and type info resolution with the SDK’s serializer options.

You can also share your feedback on Copilot code review. Take the survey.

When a CancellationToken fires during a JSON-RPC call, StreamJsonRpc's
StandardCancellationStrategy.CancelOutboundRequest() attempts to serialize
RequestId. The SDK's SystemTextJsonFormatter is configured with AOT-safe
source-generated contexts, but none of them include StreamJsonRpc.RequestId.

This causes a NotSupportedException that is unobserved by the JSON-RPC
reader, silently killing the connection and leaving sessions permanently
stuck at IsProcessing=true.

The fix adds a StreamJsonRpcTypeInfoResolver — a reflection-based fallback
IJsonTypeInfoResolver — as the last entry in the TypeInfoResolverChain.
This ensures RequestId (and any other StreamJsonRpc-internal types) can
be serialized when cancellation fires, without disrupting the AOT-safe
source-generated path for all SDK-owned types.

The reflection fallback is only reached for types not covered by the
five source-generated contexts, so the AOT/trimming suppressions
(IL2026, IL3050) are targeted and justified.

Fixes: PureWeen/PolyPilot#319
Affected versions: 0.1.30+ (introduced by NativeAOT work in PR github#81)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@PureWeen PureWeen force-pushed the fix/dotnet-requestid-aot-serialization branch from d512e5a to 9d6a63b Compare March 10, 2026 20:58
@stephentoub
Copy link
Collaborator

stephentoub commented Mar 10, 2026

Hmm... why can't this instead be done just by adding [JsonSerializable(typeof(RequestId))] to one of the existing json serialization contexts, like here?
https://github.com/PureWeen/copilot-sdk/blob/d512e5a718a1917d2130f5e21af99f57eadcc76b/dotnet/src/Client.cs#L1648

EDIT: Nevermind, I see the PR comment: "RequestId couldn't be added via [JsonSerializable] because it has [JsonConverter(typeof(RequestIdSTJsonConverter))] and that converter is internal to StreamJsonRpc (SYSLIB1220). The converter also can't be used directly (CS0122)" Can you file an issue on StreamJsonRpc about that? Such converters need to be public for this very reason.

@PureWeen
Copy link
Author

Hmm... why can't this instead be done just by adding [JsonSerializable(typeof(RequestId))] to one of the existing json serialization contexts, like here? https://github.com/PureWeen/copilot-sdk/blob/d512e5a718a1917d2130f5e21af99f57eadcc76b/dotnet/src/Client.cs#L1648

EDIT: Nevermind, I see the PR comment: "RequestId couldn't be added via [JsonSerializable] because it has [JsonConverter(typeof(RequestIdSTJsonConverter))] and that converter is internal to StreamJsonRpc (SYSLIB1220). The converter also can't be used directly (CS0122)" Can you file an issue on StreamJsonRpc about that? Such converters need to be public for this very reason.

microsoft/vs-streamjsonrpc#1407

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.

Investigate Copilot SDK cancellation serialization bug

3 participants