Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ namespace Microsoft.Extensions.AI;

// These should be added in once they're no longer [Experimental]. If they're included while still
// experimental, any JsonSerializerContext that includes AIContent will incur errors about using
// experimental types in its source generated files.
// experimental types in its source generated files. When [Experimental] is removed from these types,
// these lines should be uncommented and the corresponding lines in AIJsonUtilities.CreateDefaultOptions
// as well as the [JsonSerializable] attributes for them on the JsonContext should be removed.
// [JsonDerivedType(typeof(FunctionApprovalRequestContent), typeDiscriminator: "functionApprovalRequest")]
// [JsonDerivedType(typeof(FunctionApprovalResponseContent), typeDiscriminator: "functionApprovalResponse")]
// [JsonDerivedType(typeof(McpServerToolCallContent), typeDiscriminator: "mcpServerToolCall")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ private static JsonSerializerOptions CreateDefaultOptions()
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};

// Temporary workaround: these types are [Experimental] and can't be added as [JsonDerivedType] on AIContent yet,
// or else consuming assemblies that used source generation with AIContent would implicitly reference them.
// Once they're no longer [Experimental] and added as [JsonDerivedType] on AIContent, these lines should be removed.
AddAIContentType(options, typeof(FunctionApprovalRequestContent), typeDiscriminatorId: "functionApprovalRequest", checkBuiltIn: false);
AddAIContentType(options, typeof(FunctionApprovalResponseContent), typeDiscriminatorId: "functionApprovalResponse", checkBuiltIn: false);
AddAIContentType(options, typeof(McpServerToolCallContent), typeDiscriminatorId: "mcpServerToolCall", checkBuiltIn: false);
AddAIContentType(options, typeof(McpServerToolResultContent), typeDiscriminatorId: "mcpServerToolResult", checkBuiltIn: false);
AddAIContentType(options, typeof(McpServerToolApprovalRequestContent), typeDiscriminatorId: "mcpServerToolApprovalRequest", checkBuiltIn: false);
AddAIContentType(options, typeof(McpServerToolApprovalResponseContent), typeDiscriminatorId: "mcpServerToolApprovalResponse", checkBuiltIn: false);

if (JsonSerializer.IsReflectionEnabledByDefault)
{
// If reflection-based serialization is enabled by default, use it as a fallback for all other types.
Expand Down Expand Up @@ -116,6 +126,16 @@ private static JsonSerializerOptions CreateDefaultOptions()
[JsonSerializable(typeof(Embedding<double>))]
[JsonSerializable(typeof(AIContent))]
[JsonSerializable(typeof(AIFunctionArguments))]

// Temporary workaround:
// These should be added in once they're no longer [Experimental] and included via [JsonDerivedType] on AIContent.
[JsonSerializable(typeof(FunctionApprovalRequestContent))]
[JsonSerializable(typeof(FunctionApprovalResponseContent))]
[JsonSerializable(typeof(McpServerToolCallContent))]
[JsonSerializable(typeof(McpServerToolResultContent))]
[JsonSerializable(typeof(McpServerToolApprovalRequestContent))]
[JsonSerializable(typeof(McpServerToolApprovalResponseContent))]

[EditorBrowsable(EditorBrowsableState.Never)] // Never use JsonContext directly, use DefaultOptions instead.
private sealed partial class JsonContext : JsonSerializerContext;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public static void AddAIContentType<TContent>(this JsonSerializerOptions options
_ = Throw.IfNull(options);
_ = Throw.IfNull(typeDiscriminatorId);

AddAIContentTypeCore(options, typeof(TContent), typeDiscriminatorId);
AddAIContentType(options, typeof(TContent), typeDiscriminatorId, checkBuiltIn: true);
}

/// <summary>
Expand All @@ -56,10 +56,10 @@ public static void AddAIContentType(this JsonSerializerOptions options, Type con

if (!typeof(AIContent).IsAssignableFrom(contentType))
{
Throw.ArgumentException(nameof(contentType), "The content type must derive from AIContent.");
Throw.ArgumentException(nameof(contentType), $"The content type must derive from {nameof(AIContent)}.");
}

AddAIContentTypeCore(options, contentType, typeDiscriminatorId);
AddAIContentType(options, contentType, typeDiscriminatorId, checkBuiltIn: true);
}

/// <summary>Serializes the supplied values and computes a string hash of the resulting JSON.</summary>
Expand Down Expand Up @@ -186,11 +186,11 @@ static void NormalizeJsonNode(JsonNode? node)
}
}

private static void AddAIContentTypeCore(JsonSerializerOptions options, Type contentType, string typeDiscriminatorId)
private static void AddAIContentType(JsonSerializerOptions options, Type contentType, string typeDiscriminatorId, bool checkBuiltIn)
{
if (contentType.Assembly == typeof(AIContent).Assembly)
if (checkBuiltIn && (contentType.Assembly == typeof(AIContent).Assembly))
{
Throw.ArgumentException(nameof(contentType), "Cannot register built-in AI content types.");
Throw.ArgumentException(nameof(contentType), $"Cannot register built-in {nameof(AIContent)} types.");
}

IJsonTypeInfoResolver resolver = options.TypeInfoResolver ?? DefaultOptions.TypeInfoResolver!;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Text.Json;
using Xunit;

Expand Down Expand Up @@ -53,4 +54,40 @@ public void Serialization_Roundtrips()
Assert.NotNull(deserialized.AdditionalProperties);
Assert.Single(deserialized.AdditionalProperties);
}

[Fact]
public void Serialization_DerivedTypes_Roundtrips()
{
ChatMessage message = new(ChatRole.User,
[
new TextContent("a"),
new TextReasoningContent("reasoning text"),
new DataContent(new byte[] { 1, 2, 3 }, "application/octet-stream"),
new UriContent("http://example.com", "application/json"),
new ErrorContent("error message"),
new FunctionCallContent("call123", "functionName", new Dictionary<string, object?> { { "param1", 123 } }),
new FunctionResultContent("call123", "result data"),
new HostedFileContent("file123"),
new HostedVectorStoreContent("vectorStore123"),
new UsageContent(new UsageDetails { InputTokenCount = 10, OutputTokenCount = 20, TotalTokenCount = 30 }),
new FunctionApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary<string, object?> { { "param1", 123 } })),
new FunctionApprovalResponseContent("request123", approved: true, new FunctionCallContent("call123", "functionName", new Dictionary<string, object?> { { "param1", 123 } })),
new McpServerToolCallContent("call123", "myTool", "myServer"),
new McpServerToolResultContent("call123"),
new McpServerToolApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")),
new McpServerToolApprovalResponseContent("request123", approved: true)
]);

var serialized = JsonSerializer.Serialize(message, AIJsonUtilities.DefaultOptions);
ChatMessage? deserialized = JsonSerializer.Deserialize<ChatMessage>(serialized, AIJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);

Assert.Equal(message.Role, deserialized.Role);
Assert.Equal(message.Contents.Count, deserialized.Contents.Count);
for (int i = 0; i < message.Contents.Count; i++)
{
Assert.NotNull(message.Contents[i]);
Assert.Equal(message.Contents[i].GetType(), deserialized.Contents[i].GetType());
}
}
}
Loading