.Net: fix: fall back to ToString() when logging function results with unregistered types (#13884)

Fixes #13681

## Problem

When a `KernelFunction` returns a value whose runtime type is not
registered in `AbstractionsJsonContext` (e.g.
`Microsoft.Extensions.AI.TextContent` returned by an MCP tool via
`.AsKernelFunction()`), the function result logging in
AOT/source-generation mode throws `NotSupportedException` during JSON
serialization. The exception is caught but produces an unhelpful log
entry:

```
Function SomePlugin-SomeTool result: Failed to log function result value
System.NotSupportedException: JsonTypeInfo metadata for type 'Microsoft.Extensions.AI.TextContent' was not provided by TypeInfoResolver of type 'Microsoft.SemanticKernel.AbstractionsJsonContext'.
```

## Solution

Add a `ToString()` fallback in the `NotSupportedException` catch block
of `LogFunctionResultValueInternal`. When JSON serialization fails
because the runtime type is absent from the source-generated context,
the logger now calls `resultValue?.Value?.ToString()` to produce useful
output instead of the generic error message.

## Testing

- Added `ItShouldFallBackToToStringWhenJsonSerializationIsNotSupported`
unit test that uses a restricted source-generated
`JsonSerializerContext` (containing only `object` and
`IDictionary<string, object?>`) to simulate the AOT scenario, verifying
that `ToString()` output appears in the log instead of the failure
message.
- All existing `ItShouldLogFunctionResultOfAnyType` test cases are
unchanged.

---------

Co-authored-by: octo-patch <[email protected]>
Co-authored-by: SergeyMenshykh <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: SergeyMenshykh <[email protected]>
Co-authored-by: Copilot <[email protected]>
This commit is contained in:
Octopus
2026-05-01 08:31:14 +00:00
committed by GitHub
co-authored by octo-patch SergeyMenshykh Copilot SergeyMenshykh Copilot
parent 6e1ab9dee5
commit 006a5d9a9b
2 changed files with 55 additions and 2 deletions
@@ -161,7 +161,22 @@ internal static partial class KernelFunctionLogMessages
}
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 (Exception toStringEx)
{
s_logFunctionResultValue(
logger,
pluginName,
functionName,
"Failed to log function result value",
new AggregateException(ex, toStringEx));
}
}
}
}
@@ -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;
@@ -10,7 +11,7 @@ using Xunit;
namespace SemanticKernel.UnitTests.Functions;
public class KernelFunctionLogMessagesTests
public partial class KernelFunctionLogMessagesTests
{
[Theory]
[InlineData(typeof(string))]
@@ -48,9 +49,46 @@ public class KernelFunctionLogMessagesTests
It.IsAny<Func<It.IsAnyType, Exception?, string>>()));
}
[Fact]
public void ItShouldFallBackToToStringWhenJsonSerializationIsNotSupported()
{
// Arrange
var logger = new Mock<ILogger>();
logger.Setup(l => l.IsEnabled(It.IsAny<LogLevel>())).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
// MEAI type (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<It.IsAnyType>((o, _) => o.ToString() == "Function p1-f1 result: TypeNotInJsonContext()"),
null,
It.IsAny<Func<It.IsAnyType, Exception?, string>>()));
}
private sealed class User
{
[JsonPropertyName("name")]
public string? Name { get; set; }
}
private sealed class TypeNotInJsonContext
{
public override string ToString() => "TypeNotInJsonContext()";
}
[JsonSerializable(typeof(IDictionary<string, object?>))]
[JsonSerializable(typeof(object))]
private sealed partial class RestrictedJsonContext : JsonSerializerContext { }
}