diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs
index 3001520999..965af36ec0 100644
--- a/dotnet/src/Generated/Rpc.cs
+++ b/dotnet/src/Generated/Rpc.cs
@@ -31,6 +31,7 @@ public sealed class PingResult
public long ProtocolVersion { get; set; }
/// ISO 8601 timestamp when the server handled the ping.
+ [JsonConverter(typeof(GitHub.Copilot.UnixMillisecondsDateTimeOffsetConverter))]
[JsonPropertyName("timestamp")]
public DateTimeOffset Timestamp { get; set; }
}
@@ -29249,4 +29250,4 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH
[JsonSerializable(typeof(WorkspacesUpdateMetadataRequest))]
[JsonSerializable(typeof(WorkspacesWriteAutopilotObjectiveRequest))]
[JsonSerializable(typeof(WorkspacesWriteAutopilotObjectiveResult))]
-internal partial class RpcJsonContext : JsonSerializerContext;
\ No newline at end of file
+internal partial class RpcJsonContext : JsonSerializerContext;
diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs
index 6b511117cd..4167f8426e 100644
--- a/dotnet/src/Types.cs
+++ b/dotnet/src/Types.cs
@@ -3845,6 +3845,7 @@ public sealed class PingResponse
///
/// ISO 8601 timestamp when the ping was processed.
///
+ [JsonConverter(typeof(UnixMillisecondsDateTimeOffsetConverter))]
public DateTimeOffset Timestamp { get; set; }
///
/// Protocol version supported by the server.
diff --git a/dotnet/src/UnixMillisecondsDateTimeOffsetConverter.cs b/dotnet/src/UnixMillisecondsDateTimeOffsetConverter.cs
index 8e176fbafa..797e850d5a 100644
--- a/dotnet/src/UnixMillisecondsDateTimeOffsetConverter.cs
+++ b/dotnet/src/UnixMillisecondsDateTimeOffsetConverter.cs
@@ -3,18 +3,30 @@
*--------------------------------------------------------------------------------------------*/
using System.ComponentModel;
+using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace GitHub.Copilot;
-/// Converts between JSON numeric milliseconds-since-Unix-epoch and .
+/// Converts JSON numeric milliseconds-since-Unix-epoch or ISO 8601 strings to .
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class UnixMillisecondsDateTimeOffsetConverter : JsonConverter
{
///
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ string? value = reader.GetString();
+ if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out long milliseconds))
+ {
+ return DateTimeOffset.FromUnixTimeMilliseconds(milliseconds);
+ }
+
+ return reader.GetDateTimeOffset();
+ }
+
// The CLI may serialize the epoch-millisecond timestamp as a JSON integer
// or as a floating-point number (e.g. 1700000000000.0). GetInt64 throws on a
// fractional token, so fall back to reading a double and truncating.
diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs
index 9108a81343..2700a33b67 100644
--- a/dotnet/test/Unit/SerializationTests.cs
+++ b/dotnet/test/Unit/SerializationTests.cs
@@ -913,6 +913,82 @@ public void AgentStopHookInput_DeserializesWireFields_WithSdkOptions()
Assert.Equal(DateTimeOffset.FromUnixTimeMilliseconds(1700000000000), input.Timestamp);
}
+ [Fact]
+ public void PingResponse_DeserializesIsoTimestamp_WithSdkOptions()
+ {
+ var options = GetSerializerOptions();
+ var response = JsonSerializer.Deserialize(
+ """{"message":"pong","timestamp":"2026-05-21T08:29:54.042Z","protocolVersion":3}""",
+ options);
+
+ Assert.NotNull(response);
+ Assert.Equal("pong", response.Message);
+ Assert.Equal(3, response.ProtocolVersion);
+ Assert.Equal(DateTimeOffset.Parse("2026-05-21T08:29:54.042Z"), response.Timestamp);
+ }
+
+ [Fact]
+ public void PingResponse_DeserializesEpochMillisecondsTimestamp_WithSdkOptions()
+ {
+ var options = GetSerializerOptions();
+ var response = JsonSerializer.Deserialize(
+ """{"message":"pong","timestamp":1779352370134,"protocolVersion":3}""",
+ options);
+
+ Assert.NotNull(response);
+ Assert.Equal(DateTimeOffset.FromUnixTimeMilliseconds(1779352370134), response.Timestamp);
+ }
+
+ [Fact]
+ public void PingResponse_DeserializesStringEpochMillisecondsTimestamp_WithSdkOptions()
+ {
+ var options = GetSerializerOptions();
+ var response = JsonSerializer.Deserialize(
+ """{"message":"pong","timestamp":"1779352370134","protocolVersion":3}""",
+ options);
+
+ Assert.NotNull(response);
+ Assert.Equal(DateTimeOffset.FromUnixTimeMilliseconds(1779352370134), response.Timestamp);
+ }
+
+ [Fact]
+ public void RpcPingResult_DeserializesIsoTimestamp_WithSdkOptions()
+ {
+ var options = GetSerializerOptions();
+ var result = JsonSerializer.Deserialize(
+ """{"message":"pong","timestamp":"2026-05-21T08:29:54.042Z","protocolVersion":3}""",
+ options);
+
+ Assert.NotNull(result);
+ Assert.Equal("pong", result.Message);
+ Assert.Equal(3, result.ProtocolVersion);
+ Assert.Equal(DateTimeOffset.Parse("2026-05-21T08:29:54.042Z"), result.Timestamp);
+ }
+
+ [Fact]
+ public void RpcPingResult_DeserializesEpochMillisecondsTimestamp_WithSdkOptions()
+ {
+ var options = GetSerializerOptions();
+ var result = JsonSerializer.Deserialize(
+ """{"message":"pong","timestamp":1779352370134,"protocolVersion":3}""",
+ options);
+
+ Assert.NotNull(result);
+ Assert.Equal(DateTimeOffset.FromUnixTimeMilliseconds(1779352370134), result.Timestamp);
+ }
+
+ [Fact]
+ public void RpcPingResult_DeserializesStringEpochMillisecondsTimestamp_WithSdkOptions()
+ {
+ var options = GetSerializerOptions();
+ var result = JsonSerializer.Deserialize(
+ """{"message":"pong","timestamp":"1779352370134","protocolVersion":3}""",
+ options);
+
+ Assert.NotNull(result);
+ Assert.Equal(DateTimeOffset.FromUnixTimeMilliseconds(1779352370134), result.Timestamp);
+ }
+
[Fact]
public void AgentStopHookOutput_SerializesBlockDecision_WithSdkOptions()
{
diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts
index 97fcebea67..af064d10e4 100644
--- a/scripts/codegen/csharp.ts
+++ b/scripts/codegen/csharp.ts
@@ -492,6 +492,9 @@ function isMillisecondsDurationProperty(propName: string | undefined, schema: JS
return isDurationProperty(schema) && !isSecondsDurationPropertyName(propName);
}
+function isFlexiblePingTimestampProperty(className: string, propName: string, schema: JSONSchema7): boolean {
+ return className === "PingResult" && propName === "timestamp" && schema.format === "date-time";
+}
const COPYRIGHT = `/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
@@ -1736,6 +1739,9 @@ function emitRpcClass(
if (isSchemaDeprecated(prop)) pushObsoleteAttributes(lines, " ");
if (isSchemaExperimental(prop)) pushExperimentalAttribute(lines, " ");
if (isMillisecondsDurationProperty(propName, prop)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`);
+ if (isFlexiblePingTimestampProperty(inlineTypeParentName, propName, prop)) {
+ lines.push(` [JsonConverter(typeof(GitHub.Copilot.UnixMillisecondsDateTimeOffsetConverter))]`);
+ }
const propVisibility = pushCSharpInternalAttribute(lines, prop);
lines.push(` [JsonPropertyName("${propName}")]`);