From a0db521af8b64db0c07fd232f7e1824ff08b6503 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Wed, 13 May 2026 03:39:47 +0800 Subject: [PATCH 1/2] .Net: fix function enum argument deserialization --- .../src/Schema/KernelJsonSchemaBuilder.cs | 4 +++ .../Functions/KernelFunctionFromMethod.cs | 10 +++--- .../KernelFunctionFromMethodTests1.cs | 33 ++++++++++++++++++- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/dotnet/src/InternalUtilities/src/Schema/KernelJsonSchemaBuilder.cs b/dotnet/src/InternalUtilities/src/Schema/KernelJsonSchemaBuilder.cs index 82f82895b96b..ad4089166d66 100644 --- a/dotnet/src/InternalUtilities/src/Schema/KernelJsonSchemaBuilder.cs +++ b/dotnet/src/InternalUtilities/src/Schema/KernelJsonSchemaBuilder.cs @@ -26,6 +26,10 @@ internal static class KernelJsonSchemaBuilder private static JsonSerializerOptions? s_options; internal static readonly AIJsonSchemaCreateOptions s_schemaOptions = new(); + [RequiresUnreferencedCode("Uses JsonStringEnumConverter and DefaultJsonTypeInfoResolver classes, making it incompatible with AOT scenarios.")] + [RequiresDynamicCode("Uses JsonStringEnumConverter and DefaultJsonTypeInfoResolver classes, making it incompatible with AOT scenarios.")] + internal static JsonSerializerOptions GetDefaultOptionsForSerialization() => GetDefaultOptions(); + private static readonly JsonElement s_trueSchemaAsObject = JsonElement.Parse("{}"); private static readonly JsonElement s_falseSchemaAsObject = JsonElement.Parse("""{"not":true}"""); diff --git a/dotnet/src/SemanticKernel.Core/Functions/KernelFunctionFromMethod.cs b/dotnet/src/SemanticKernel.Core/Functions/KernelFunctionFromMethod.cs index 9385ae02bcaf..eb8590bf775d 100644 --- a/dotnet/src/SemanticKernel.Core/Functions/KernelFunctionFromMethod.cs +++ b/dotnet/src/SemanticKernel.Core/Functions/KernelFunctionFromMethod.cs @@ -789,18 +789,20 @@ private static bool TryToDeserializeValue(object value, Type targetType, JsonSer { try { + JsonSerializerOptions effectiveOptions = jsonSerializerOptions ?? KernelJsonSchemaBuilder.GetDefaultOptionsForSerialization(); + deserializedValue = value switch { - JsonDocument document => document.Deserialize(targetType, jsonSerializerOptions), - JsonNode node => node.Deserialize(targetType, jsonSerializerOptions), - JsonElement element => element.Deserialize(targetType, jsonSerializerOptions), + JsonDocument document => document.Deserialize(targetType, effectiveOptions), + JsonNode node => node.Deserialize(targetType, effectiveOptions), + JsonElement element => element.Deserialize(targetType, effectiveOptions), // The JSON can be represented by other data types from various libraries. For example, JObject, JToken, and JValue from the Newtonsoft.Json library. // Since we don't take dependencies on these libraries and don't have access to the types here, // the only way to deserialize those types is to convert them to a string first by calling the 'ToString' method. // Attempting to use the 'JsonSerializer.Serialize' method, instead of calling the 'ToString' directly on those types, can lead to unpredictable outcomes. // For instance, the JObject for { "id": 28 } JSON is serialized into the string "{ "Id": [] }", and the deserialization fails with the // following exception - "The JSON value could not be converted to System.Int32. Path: $.Id | LineNumber: 0 | BytePositionInLine: 7." - _ => JsonSerializer.Deserialize(value.ToString()!, targetType, jsonSerializerOptions) + _ => JsonSerializer.Deserialize(value.ToString()!, targetType, effectiveOptions) }; return true; diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionFromMethodTests1.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionFromMethodTests1.cs index 2c154029617a..c59a60e48260 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionFromMethodTests1.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionFromMethodTests1.cs @@ -1413,13 +1413,31 @@ public async Task ItCanDeserializeJsonStringAsync() var func = KernelFunctionFactory.CreateFromMethod((CustomTypeForJsonTests param) => { actualArgValue = param; }); // Act - var res = await func.InvokeAsync(this._kernel, new() { ["param"] = jsonString }); + _ = await func.InvokeAsync(this._kernel, new() { ["param"] = jsonString }); // Assert Assert.NotNull(actualArgValue); Assert.Equal(28, actualArgValue.Id); } + [Fact] + public async Task ItCanDeserializeStringEnumsWithDefaultJsonOptionsAsync() + { + // Arrange + var jsonString = @"[{""unit"":""hours""}]"; + Reminder[]? actualArgValue = null; + + var func = KernelFunctionFactory.CreateFromMethod((Reminder[] param) => { actualArgValue = param; }); + + // Act + var res = await func.InvokeAsync(this._kernel, new() { ["param"] = jsonString }); + + // Assert + Assert.NotNull(actualArgValue); + Assert.Single(actualArgValue); + Assert.Equal(ReminderUnit.Hours, actualArgValue[0].Unit); + } + [Fact] public async Task ItCanDeserializeThirdPartyJsonPrimitivesAsync() { @@ -1586,6 +1604,19 @@ private sealed class CustomTypeForJsonTests public int Id { get; set; } } + private sealed class Reminder + { + [JsonPropertyName("unit")] + public ReminderUnit Unit { get; set; } + } + + private enum ReminderUnit + { + Minutes, + Hours, + Days + } + private sealed class ThirdPartyJsonPrimitive(string jsonToReturn) { public override string ToString() => jsonToReturn; From c4c6ec016cbe7c946d77112c5b4cd12bd5525c94 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Thu, 14 May 2026 22:13:35 +0800 Subject: [PATCH 2/2] fix: clarify default json options use --- .../src/InternalUtilities/src/Schema/KernelJsonSchemaBuilder.cs | 2 +- .../SemanticKernel.Core/Functions/KernelFunctionFromMethod.cs | 2 +- .../Functions/KernelFunctionFromMethodTests1.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/src/InternalUtilities/src/Schema/KernelJsonSchemaBuilder.cs b/dotnet/src/InternalUtilities/src/Schema/KernelJsonSchemaBuilder.cs index ad4089166d66..0ddd57d604fa 100644 --- a/dotnet/src/InternalUtilities/src/Schema/KernelJsonSchemaBuilder.cs +++ b/dotnet/src/InternalUtilities/src/Schema/KernelJsonSchemaBuilder.cs @@ -28,7 +28,7 @@ internal static class KernelJsonSchemaBuilder [RequiresUnreferencedCode("Uses JsonStringEnumConverter and DefaultJsonTypeInfoResolver classes, making it incompatible with AOT scenarios.")] [RequiresDynamicCode("Uses JsonStringEnumConverter and DefaultJsonTypeInfoResolver classes, making it incompatible with AOT scenarios.")] - internal static JsonSerializerOptions GetDefaultOptionsForSerialization() => GetDefaultOptions(); + internal static JsonSerializerOptions GetDefaultJsonSerializerOptions() => GetDefaultOptions(); private static readonly JsonElement s_trueSchemaAsObject = JsonElement.Parse("{}"); private static readonly JsonElement s_falseSchemaAsObject = JsonElement.Parse("""{"not":true}"""); diff --git a/dotnet/src/SemanticKernel.Core/Functions/KernelFunctionFromMethod.cs b/dotnet/src/SemanticKernel.Core/Functions/KernelFunctionFromMethod.cs index eb8590bf775d..b6859f0658b4 100644 --- a/dotnet/src/SemanticKernel.Core/Functions/KernelFunctionFromMethod.cs +++ b/dotnet/src/SemanticKernel.Core/Functions/KernelFunctionFromMethod.cs @@ -789,7 +789,7 @@ private static bool TryToDeserializeValue(object value, Type targetType, JsonSer { try { - JsonSerializerOptions effectiveOptions = jsonSerializerOptions ?? KernelJsonSchemaBuilder.GetDefaultOptionsForSerialization(); + JsonSerializerOptions effectiveOptions = jsonSerializerOptions ?? KernelJsonSchemaBuilder.GetDefaultJsonSerializerOptions(); deserializedValue = value switch { diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionFromMethodTests1.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionFromMethodTests1.cs index c59a60e48260..ec8850e5f66c 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionFromMethodTests1.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionFromMethodTests1.cs @@ -1430,7 +1430,7 @@ public async Task ItCanDeserializeStringEnumsWithDefaultJsonOptionsAsync() var func = KernelFunctionFactory.CreateFromMethod((Reminder[] param) => { actualArgValue = param; }); // Act - var res = await func.InvokeAsync(this._kernel, new() { ["param"] = jsonString }); + _ = await func.InvokeAsync(this._kernel, new() { ["param"] = jsonString }); // Assert Assert.NotNull(actualArgValue);