From 77eaa7f2d3bc54e79b9843c29d0e6d3fec4adfd5 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Sat, 18 Apr 2026 10:04:50 +0800 Subject: [PATCH] fix: fall back to ToString() when JSON serialization fails in function result logging (fixes #13681) When a KernelFunction returns a type not registered in AbstractionsJsonContext (e.g. Microsoft.Extensions.AI.TextContent from MCP tools), and JSON serialization is attempted with AOT-safe JsonSerializerOptions, a NotSupportedException is thrown and swallowed, causing the log message to show "Failed to log function result value" instead of useful content. This change adds a ToString() fallback in the NotSupportedException catch block so that result values still produce meaningful log output even when their runtime type is absent from the source-generated JSON context. --- .../Functions/KernelFunctionLogMessages.cs | 12 +++++- .../KernelFunctionLogMessagesTests.cs | 38 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunctionLogMessages.cs b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunctionLogMessages.cs index 42c4b7f6e6a9..4b0518814c9f 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunctionLogMessages.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunctionLogMessages.cs @@ -161,7 +161,17 @@ private static void LogFunctionResultValueInternal(this ILogger logger, string? } catch (NotSupportedException ex) { - s_logFunctionResultValue(logger, pluginName, functionName, "Failed to log function result value", ex); + // Fall back to ToString() when JSON serialization isn't supported for this type + // (e.g. Microsoft.Extensions.AI.TextContent is not registered in AbstractionsJsonContext) + try + { + var toStringValue = resultValue?.Value?.ToString() ?? string.Empty; + s_logFunctionResultValue(logger, pluginName, functionName, toStringValue, null); + } + catch + { + s_logFunctionResultValue(logger, pluginName, functionName, "Failed to log function result value", ex); + } } } } diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionLogMessagesTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionLogMessagesTests.cs index ec2642fa12e1..2e618b79c96f 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionLogMessagesTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionLogMessagesTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Generic; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; @@ -48,9 +49,46 @@ public void ItShouldLogFunctionResultOfAnyType(Type resultType) It.IsAny>())); } + [Fact] + public void ItShouldFallBackToToStringWhenJsonSerializationIsNotSupported() + { + // Arrange + var logger = new Mock(); + logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); + + // TypeNotInJsonContext cannot be cast to string and is not registered in the restricted JSON context + var unserializableValue = new TypeNotInJsonContext(); + var functionResult = new FunctionResult(KernelFunctionFactory.CreateFromMethod(() => { }), unserializableValue); + + // Use a restricted JsonSerializerOptions that knows about object but not TypeNotInJsonContext, + // simulating the AOT scenario where AbstractionsJsonContext is used and an unregistered + // MEAItype (e.g. Microsoft.Extensions.AI.TextContent) is returned from an MCP tool. + var restrictedOptions = RestrictedJsonContext.Default.Options; + + // Act + logger.Object.LogFunctionResultValue("p1", "f1", functionResult, restrictedOptions); + + // Assert - ToString() fallback should have been used, not the error message + logger.Verify(l => l.Log( + LogLevel.Trace, + 0, + It.Is((o, _) => o.ToString() == "Function p1-f1 result: TypeNotInJsonContext()"), + null, + It.IsAny>())); + } + private sealed class User { [JsonPropertyName("name")] public string? Name { get; set; } } + + private sealed class TypeNotInJsonContext + { + public override string ToString() => "TypeNotInJsonContext()"; + } + + [JsonSerializable(typeof(IDictionary))] + [JsonSerializable(typeof(object))] + private sealed partial class RestrictedJsonContext : JsonSerializerContext { } }