From 820c6afe0991035b835dd1d265554ba6e787c1be Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 6 Nov 2025 17:23:37 -0500 Subject: [PATCH] .NET: Fix the ordering of chained resolvers in JsonSerializerOptions (#1974) * Fix the ordering of chained resolvers in JsonSerializerOptions We want the resolvers from AIJsonUtilities to be used before the ones from the source generator, in case the source generator emits its own copy in that assembly for the M.E.AI types. * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs * Update dotnet/src/Microsoft.Agents.AI.Mem0/Mem0JsonUtilities.cs Co-authored-by: westey <164392973+westey-m@users.noreply.github.com> * Remove unused using directive in Mem0JsonUtilities Removed unused using directive for Microsoft.Extensions.AI. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: westey <164392973+westey-m@users.noreply.github.com> --- .../AgentAbstractionsJsonUtilities.cs | 8 ++++++-- .../ChatCompletionsJsonSerializerOptions.cs | 6 ++++++ .../OpenAIHostingJsonUtilities.cs | 6 ++++++ .../Mem0JsonUtilities.cs | 9 ++++++--- .../WorkflowsJsonUtilities.cs | 5 ++++- .../Microsoft.Agents.AI/AgentJsonUtilities.cs | 6 +++++- .../InMemoryChatMessageStoreTests.cs | 17 +++++++++++++++++ 7 files changed, 50 insertions(+), 7 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentAbstractionsJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentAbstractionsJsonUtilities.cs index 8943e29c79..d5003cace0 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentAbstractionsJsonUtilities.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentAbstractionsJsonUtilities.cs @@ -50,12 +50,16 @@ public static partial class AgentAbstractionsJsonUtilities Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // same as AIJsonUtilities }; - // Chain with all supported types from Microsoft.Extensions.AI.Abstractions. + // Chain in the resolvers from both AIJsonUtilities and our source generated context. + // We want AIJsonUtilities first to ensure any M.E.AI types are handled via its resolver. + options.TypeInfoResolverChain.Clear(); + options.TypeInfoResolverChain.Add(AIJsonUtilities.DefaultOptions.TypeInfoResolver!); + options.TypeInfoResolverChain.Add(JsonContext.Default.Options.TypeInfoResolver!); + // If reflection-based serialization is enabled by default, this includes // the default type info resolver that utilizes reflection, but we need to manually // apply the same converter AIJsonUtilities adds for string-based enum serialization, // as that's not propagated as part of the resolver. - options.TypeInfoResolverChain.Add(AIJsonUtilities.DefaultOptions.TypeInfoResolver!); if (JsonSerializer.IsReflectionEnabledByDefault) { options.Converters.Add(new JsonStringEnumConverter()); diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ChatCompletions/ChatCompletionsJsonSerializerOptions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ChatCompletions/ChatCompletionsJsonSerializerOptions.cs index b009b82d29..301cae1f8f 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ChatCompletions/ChatCompletionsJsonSerializerOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ChatCompletions/ChatCompletionsJsonSerializerOptions.cs @@ -17,7 +17,13 @@ internal static class ChatCompletionsJsonSerializerOptions private static JsonSerializerOptions Create() { JsonSerializerOptions options = new(ChatCompletionsJsonContext.Default.Options); + + // Chain in the resolvers from both AgentAbstractionsJsonUtilities and our source generated context. + // We want AgentAbstractionsJsonUtilities first to ensure any M.E.AI types are handled via its resolver. + options.TypeInfoResolverChain.Clear(); options.TypeInfoResolverChain.Add(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!); + options.TypeInfoResolverChain.Add(ChatCompletionsJsonContext.Default.Options.TypeInfoResolver!); + options.MakeReadOnly(); return options; } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/OpenAIHostingJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/OpenAIHostingJsonUtilities.cs index ceac8b872f..49ceef622a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/OpenAIHostingJsonUtilities.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/OpenAIHostingJsonUtilities.cs @@ -24,7 +24,13 @@ internal static class OpenAIHostingJsonUtilities private static JsonSerializerOptions CreateDefaultOptions() { JsonSerializerOptions options = new(OpenAIHostingJsonContext.Default.Options); + + // Chain in the resolvers from both AgentAbstractionsJsonUtilities and our source generated context. + // We want AgentAbstractionsJsonUtilities first to ensure any M.E.AI types are handled via its resolver. + options.TypeInfoResolverChain.Clear(); options.TypeInfoResolverChain.Add(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!); + options.TypeInfoResolverChain.Add(OpenAIHostingJsonContext.Default.Options.TypeInfoResolver!); + options.MakeReadOnly(); return options; } diff --git a/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0JsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0JsonUtilities.cs index eb50d31f70..d139cb0f76 100644 --- a/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0JsonUtilities.cs +++ b/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0JsonUtilities.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; -using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI.Mem0; @@ -44,8 +43,12 @@ public static partial class Mem0JsonUtilities Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // same as in AIJsonUtilities }; - // Chain with all supported types from Microsoft.Extensions.AI.Abstractions. - options.TypeInfoResolverChain.Add(AIJsonUtilities.DefaultOptions.TypeInfoResolver!); + // Chain in the resolvers from both AgentAbstractionsJsonUtilities and our source generated context. + // We want AgentAbstractionsJsonUtilities first to ensure any M.E.AI types are handled via its resolver. + options.TypeInfoResolverChain.Clear(); + options.TypeInfoResolverChain.Add(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!); + options.TypeInfoResolverChain.Add(JsonContext.Default.Options.TypeInfoResolver!); + if (JsonSerializer.IsReflectionEnabledByDefault) { options.Converters.Add(new JsonStringEnumConverter()); diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowsJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowsJsonUtilities.cs index 752bb4bac7..d8241f4681 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowsJsonUtilities.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowsJsonUtilities.cs @@ -50,8 +50,11 @@ internal static partial class WorkflowsJsonUtilities // Copy the configuration from the source generated context. JsonSerializerOptions options = new(JsonContext.Default.Options); - // Chain with all supported types from Microsoft.Extensions.AI.Abstractions and Microsoft.Agents.AI.Abstractions. + // Chain in the resolvers from both AgentAbstractionsJsonUtilities and our source generated context. + // We want AgentAbstractionsJsonUtilities first to ensure any M.E.AI types are handled via its resolver. + options.TypeInfoResolverChain.Clear(); options.TypeInfoResolverChain.Add(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!); + options.TypeInfoResolverChain.Add(JsonContext.Default.Options.TypeInfoResolver!); options.MakeReadOnly(); return options; diff --git a/dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs index 5ef6978c00..36bef4a2af 100644 --- a/dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs +++ b/dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs @@ -44,8 +44,12 @@ internal static partial class AgentJsonUtilities Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // same as in AgentAbstractionsJsonUtilities and AIJsonUtilities }; - // Chain with all supported types from Microsoft.Agents.AI.Abstractions. + // Chain in the resolvers from both AgentAbstractionsJsonUtilities and our source generated context. + // We want AgentAbstractionsJsonUtilities first to ensure any M.E.AI types are handled via its resolver. + options.TypeInfoResolverChain.Clear(); options.TypeInfoResolverChain.Add(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!); + options.TypeInfoResolverChain.Add(JsonContext.Default.Options.TypeInfoResolver!); + if (JsonSerializer.IsReflectionEnabledByDefault) { options.Converters.Add(new JsonStringEnumConverter()); diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/InMemoryChatMessageStoreTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/InMemoryChatMessageStoreTests.cs index fedd0ce591..4c793d17f4 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/InMemoryChatMessageStoreTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/InMemoryChatMessageStoreTests.cs @@ -114,6 +114,23 @@ public class InMemoryChatMessageStoreTests Assert.Equal("B", newStore[1].Text); } + [Fact] + public async Task SerializeAndDeserializeWorksWithExperimentalContentTypesAsync() + { + var store = new InMemoryChatMessageStore + { + new ChatMessage(ChatRole.User, [new FunctionApprovalRequestContent("call123", new FunctionCallContent("call123", "some_func"))]), + new ChatMessage(ChatRole.Assistant, [new FunctionApprovalResponseContent("call123", true, new FunctionCallContent("call123", "some_func"))]) + }; + + var jsonElement = store.Serialize(); + var newStore = new InMemoryChatMessageStore(jsonElement); + + Assert.Equal(2, newStore.Count); + Assert.IsType(newStore[0].Contents[0]); + Assert.IsType(newStore[1].Contents[0]); + } + [Fact] public async Task AddMessagesAsyncWithEmptyMessagesDoesNotChangeStoreAsync() {