diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs
index 5b7474a64..803f57471 100644
--- a/dotnet/src/Client.cs
+++ b/dotnet/src/Client.cs
@@ -14,6 +14,7 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
using System.Text.RegularExpressions;
using GitHub.Copilot.SDK.Rpc;
using System.Globalization;
@@ -1226,6 +1227,12 @@ private static JsonSerializerOptions CreateSerializerOptions()
options.TypeInfoResolverChain.Add(SessionEventsJsonContext.Default);
options.TypeInfoResolverChain.Add(SDK.Rpc.RpcJsonContext.Default);
+ // StreamJsonRpc's RequestId needs serialization when CancellationToken fires during
+ // JSON-RPC operations. Its built-in converter (RequestIdSTJsonConverter) is internal,
+ // and [JsonSerializable] can't source-gen for it (SYSLIB1220), so we provide our own
+ // AOT-safe resolver + converter.
+ options.TypeInfoResolverChain.Add(new RequestIdTypeInfoResolver());
+
options.MakeReadOnly();
return options;
@@ -1640,6 +1647,50 @@ private static LogLevel MapLevel(TraceEventType eventType)
[JsonSerializable(typeof(UserInputResponse))]
internal partial class ClientJsonContext : JsonSerializerContext;
+ ///
+ /// AOT-safe type info resolver for .
+ /// StreamJsonRpc's own RequestIdSTJsonConverter is internal (SYSLIB1220/CS0122),
+ /// so we provide our own converter and wire it through
+ /// to stay fully AOT/trimming-compatible.
+ ///
+ private sealed class RequestIdTypeInfoResolver : IJsonTypeInfoResolver
+ {
+ public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options)
+ {
+ if (type == typeof(RequestId))
+ return JsonMetadataServices.CreateValueInfo(options, new RequestIdJsonConverter());
+ return null;
+ }
+ }
+
+ private sealed class RequestIdJsonConverter : JsonConverter
+ {
+ public override RequestId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ return reader.TokenType switch
+ {
+ JsonTokenType.Number => reader.TryGetInt64(out long val)
+ ? new RequestId(val)
+ : new RequestId(reader.HasValueSequence
+ ? Encoding.UTF8.GetString(reader.ValueSequence)
+ : Encoding.UTF8.GetString(reader.ValueSpan)),
+ JsonTokenType.String => new RequestId(reader.GetString()!),
+ JsonTokenType.Null => RequestId.Null,
+ _ => throw new JsonException($"Unexpected token type for RequestId: {reader.TokenType}"),
+ };
+ }
+
+ public override void Write(Utf8JsonWriter writer, RequestId value, JsonSerializerOptions options)
+ {
+ if (value.Number.HasValue)
+ writer.WriteNumberValue(value.Number.Value);
+ else if (value.String is not null)
+ writer.WriteStringValue(value.String);
+ else
+ writer.WriteNullValue();
+ }
+ }
+
[GeneratedRegex(@"listening on port ([0-9]+)", RegexOptions.IgnoreCase)]
private static partial Regex ListeningOnPortRegex();
}
diff --git a/dotnet/test/SerializationTests.cs b/dotnet/test/SerializationTests.cs
new file mode 100644
index 000000000..6fb266be1
--- /dev/null
+++ b/dotnet/test/SerializationTests.cs
@@ -0,0 +1,80 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+using Xunit;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using StreamJsonRpc;
+
+namespace GitHub.Copilot.SDK.Test;
+
+///
+/// Tests for JSON serialization compatibility, particularly for StreamJsonRpc types
+/// that are needed when CancellationTokens fire during JSON-RPC operations.
+/// This test suite verifies the fix for https://github.com/PureWeen/PolyPilot/issues/319
+///
+public class SerializationTests
+{
+ ///
+ /// Verifies that StreamJsonRpc.RequestId can be round-tripped using the SDK's configured
+ /// JsonSerializerOptions. This is critical for preventing NotSupportedException when
+ /// StandardCancellationStrategy fires during JSON-RPC operations.
+ ///
+ [Fact]
+ public void RequestId_CanBeSerializedAndDeserialized_WithSdkOptions()
+ {
+ var options = GetSerializerOptions();
+
+ // Long id
+ var jsonLong = JsonSerializer.Serialize(new RequestId(42L), options);
+ Assert.Equal("42", jsonLong);
+ Assert.Equal(new RequestId(42L), JsonSerializer.Deserialize(jsonLong, options));
+
+ // String id
+ var jsonStr = JsonSerializer.Serialize(new RequestId("req-1"), options);
+ Assert.Equal("\"req-1\"", jsonStr);
+ Assert.Equal(new RequestId("req-1"), JsonSerializer.Deserialize(jsonStr, options));
+
+ // Null id
+ var jsonNull = JsonSerializer.Serialize(RequestId.Null, options);
+ Assert.Equal("null", jsonNull);
+ Assert.Equal(RequestId.Null, JsonSerializer.Deserialize(jsonNull, options));
+ }
+
+ [Theory]
+ [InlineData(0L)]
+ [InlineData(-1L)]
+ [InlineData(long.MaxValue)]
+ public void RequestId_NumericEdgeCases_RoundTrip(long id)
+ {
+ var options = GetSerializerOptions();
+ var requestId = new RequestId(id);
+ var json = JsonSerializer.Serialize(requestId, options);
+ Assert.Equal(requestId, JsonSerializer.Deserialize(json, options));
+ }
+
+ ///
+ /// Verifies the SDK's options can resolve type info for RequestId,
+ /// ensuring AOT-safe serialization without falling back to reflection.
+ ///
+ [Fact]
+ public void SerializerOptions_CanResolveRequestIdTypeInfo()
+ {
+ var options = GetSerializerOptions();
+ var typeInfo = options.GetTypeInfo(typeof(RequestId));
+ Assert.NotNull(typeInfo);
+ Assert.Equal(typeof(RequestId), typeInfo.Type);
+ }
+
+ private static JsonSerializerOptions GetSerializerOptions()
+ {
+ var prop = typeof(CopilotClient)
+ .GetProperty("SerializerOptionsForMessageFormatter",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
+
+ var options = (JsonSerializerOptions?)prop?.GetValue(null);
+ Assert.NotNull(options);
+ return options;
+ }
+}