.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>
This commit is contained in:
Stephen Toub
2025-11-06 17:23:37 -05:00
committed by GitHub
Unverified
parent cb50f3e070
commit 820c6afe09
7 changed files with 50 additions and 7 deletions
@@ -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());
@@ -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;
}
@@ -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;
}
@@ -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());
@@ -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;
@@ -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());
@@ -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<FunctionApprovalRequestContent>(newStore[0].Contents[0]);
Assert.IsType<FunctionApprovalResponseContent>(newStore[1].Contents[0]);
}
[Fact]
public async Task AddMessagesAsyncWithEmptyMessagesDoesNotChangeStoreAsync()
{