Skip to content

Commit 541fd57

Browse files
committed
.Net: Fix enum deserialization mismatch between schema and tool calls
KernelJsonSchemaBuilder uses JsonStringEnumConverter when generating tool parameter schemas, telling LLMs to return enum values as strings. However, KernelFunctionFromMethod.TryToDeserializeValue did not use the same converter when deserializing LLM responses, causing JsonException for any function with enum parameters. Fall back to KernelJsonSchemaBuilder's default options (which include JsonStringEnumConverter) when no explicit JsonSerializerOptions are provided, ensuring schema generation and deserialization stay in sync. Closes #13589
1 parent 781881a commit 541fd57

File tree

3 files changed

+56
-2
lines changed

3 files changed

+56
-2
lines changed

dotnet/src/InternalUtilities/src/Schema/KernelJsonSchemaBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public static KernelJsonSchema Build(
6161

6262
[RequiresUnreferencedCode("Uses JsonStringEnumConverter and DefaultJsonTypeInfoResolver classes, making it incompatible with AOT scenarios.")]
6363
[RequiresDynamicCode("Uses JsonStringEnumConverter and DefaultJsonTypeInfoResolver classes, making it incompatible with AOT scenarios.")]
64-
private static JsonSerializerOptions GetDefaultOptions()
64+
internal static JsonSerializerOptions GetDefaultOptions()
6565
{
6666
if (s_options is null)
6767
{

dotnet/src/SemanticKernel.Core/Functions/KernelFunctionFromMethod.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -738,7 +738,7 @@ private static (Func<KernelFunction, Kernel, KernelArguments, CancellationToken,
738738
return jsonStringParser(element.GetString()!);
739739
}
740740

741-
if (value is not null && TryToDeserializeValue(value, type, jsonSerializerOptions, out var deserializedValue))
741+
if (value is not null && TryToDeserializeValue(value, type, jsonSerializerOptions ?? KernelJsonSchemaBuilder.GetDefaultOptions(), out var deserializedValue))
742742
{
743743
return deserializedValue;
744744
}

dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionFromMethodTests1.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,42 @@ public async Task ItSupportsConvertingAllIntegerTypesToEnumAsync(object argument
755755
Assert.Equal(expected, actual);
756756
}
757757

758+
[Theory]
759+
[InlineData("\"Sunday\"", DayOfWeek.Sunday)]
760+
[InlineData("\"Monday\"", DayOfWeek.Monday)]
761+
[InlineData("\"Thursday\"", DayOfWeek.Thursday)]
762+
[InlineData("\"Saturday\"", DayOfWeek.Saturday)]
763+
public async Task ItSupportsConvertingStringJsonElementToEnumAsync(string json, DayOfWeek expected)
764+
{
765+
// Arrange
766+
object? actual = null;
767+
var function = KernelFunctionFactory.CreateFromMethod((DayOfWeek dow) => actual = dow);
768+
769+
// Act — simulate LLM returning a string enum value as a JsonElement
770+
var result = await function.InvokeAsync(this._kernel, new() { ["dow"] = JsonDocument.Parse(json).RootElement });
771+
772+
// Assert
773+
Assert.Equal(expected, actual);
774+
}
775+
776+
[Fact]
777+
public async Task ItSupportsConvertingStringEnumNestedInObjectFromJsonElementAsync()
778+
{
779+
// Arrange — matches the issue scenario: enum nested in an object, then in an array
780+
TaskReminder[]? actual = null;
781+
var function = KernelFunctionFactory.CreateFromMethod((TaskReminder[] reminders) => actual = reminders);
782+
var json = """[{"unit":"Days","value":3}]""";
783+
784+
// Act — simulate LLM returning a JSON array with string enum values
785+
var result = await function.InvokeAsync(this._kernel, new() { ["reminders"] = JsonDocument.Parse(json).RootElement });
786+
787+
// Assert
788+
Assert.NotNull(actual);
789+
Assert.Single(actual);
790+
Assert.Equal(ReminderUnit.Days, actual[0].Unit);
791+
Assert.Equal(3, actual[0].Value);
792+
}
793+
758794
[TypeConverter(typeof(MyCustomTypeConverter))]
759795
private sealed class MyCustomType
760796
{
@@ -1590,4 +1626,22 @@ private sealed class ThirdPartyJsonPrimitive(string jsonToReturn)
15901626
{
15911627
public override string ToString() => jsonToReturn;
15921628
}
1629+
1630+
private enum ReminderUnit
1631+
{
1632+
Minutes,
1633+
Hours,
1634+
Days,
1635+
}
1636+
1637+
#pragma warning disable CA1812 // Avoid uninstantiated internal classes
1638+
private sealed class TaskReminder
1639+
#pragma warning restore CA1812 // Avoid uninstantiated internal classes
1640+
{
1641+
[JsonPropertyName("unit")]
1642+
public ReminderUnit Unit { get; set; }
1643+
1644+
[JsonPropertyName("value")]
1645+
public int Value { get; set; }
1646+
}
15931647
}

0 commit comments

Comments
 (0)