[Microsoft.Extensions.AI] AIFunctionFactory does not tolerate a JsonElement of ValueKind String as a JSON-stringified argument (gap in #6572)
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 68/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Quiet
- Tech stack
- csharp, json
- Domain
- ai, backend-api-design
Research direction
Start in src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs at GetParameterMarshaller around line 889, and compare the JsonElement arm with the existing raw-string IsPotentiallyJson/MarshallViaJsonRoundtrip path. Use the reported repro to verify that a JsonElement with ValueKind.String containing a valid JSON argument succeeds like a raw string, while ordinary string parameters and object-valued JsonElements retain their existing behavior.
Written by the indexing model from the issue text.
Description
Package: Microsoft.Extensions.AI.Abstractions 10.4.1 (code unchanged on main at tag v10.4.1)
Summary
#6572 ("AIFunctionFactory: tolerate JSON string function parameters", fixing #6544) made
AIFunctionFactory's parameter marshaller tolerant of a parameter whose value is a JSON string
wrapping the real argument (e.g. "{\"a\":1}" for an object parameter). That tolerance
(IsPotentiallyJson + MarshallViaJsonRoundtrip) is only reached when the argument value is a
raw .NET string.
When the same JSON-stringified object arrives as a JsonElement whose ValueKind == String,
the marshaller takes the earlier JsonElement arm and calls
JsonSerializer.Deserialize(element, typeInfo) directly, which throws before the tolerant path runs.
This is the exact shape produced when a model double-encodes a tool-call argument (emits
"input":"{...}" instead of "input":{...}) and the arguments JSON is parsed into JsonElements
(as FunctionInvokingChatClient-backed pipelines do). The failure is intermittent from the model's
side — the model usually emits the correct nested object and only occasionally stringifies it — but
whenever that shape occurs, the deserialization failure below is deterministic. A raw string with the
identical content is handled fine; a JsonElement string is not.
JsonElement element => JsonSerializer.Deserialize(element, typeInfo), // throws for ValueKind == String
JsonDocument doc => JsonSerializer.Deserialize(doc, typeInfo),
JsonNode node => JsonSerializer.Deserialize(node, typeInfo),
_ => MarshallViaJsonRoundtrip(value), // <- IsPotentiallyJson tolerance lives here
Repro (net9.0, Microsoft.Extensions.AI.Abstractions 10.4.1)
using System.Text.Json;
using Microsoft.Extensions.AI;
public sealed class Input { public string A { get; init; } = ""; public string B { get; init; } = ""; }
var func = AIFunctionFactory.Create((Input input) => "ok", new AIFunctionFactoryOptions { Name = "f" });
string inner = "{\"a\":\"x\",\"b\":\"y\"}";
// A: JsonElement of ValueKind String -> THROWS
await Invoke(JsonSerializer.SerializeToElement(inner));
// B: raw .NET string (same content) -> OK (tolerated by #6572)
await Invoke(inner);
// C: JsonElement of ValueKind Object -> OK
await Invoke(JsonSerializer.Deserialize<JsonElement>(inner));
async Task Invoke(object value)
{
try { Console.WriteLine("OK: " + await func.InvokeAsync(new AIFunctionArguments { ["input"] = value })); }
catch (Exception ex) { Console.WriteLine(ex.GetType().Name + ": " + ex.Message); }
}
Output:
JsonException: The JSON value could not be converted to Input. Path: $ | LineNumber: 0 | BytePositionInLine: 17.
OK: ok
OK: ok
Expected
A JsonElement of ValueKind == String whose content decodes to a valid argument should be
tolerated the same way a raw string is (parity with #6572), rather than throwing JsonException.
Actual
System.Text.Json.JsonException: The JSON value could not be converted to <T>. Path: $ thrown from
ObjectDefaultConverter<T>.OnTryRead via the GetParameterMarshaller delegate. In a
FunctionInvokingChatClient pipeline this surfaces as a failed tool invocation.
Why this affects real first-party clients (not a synthetic-only case)
The failing shape — an argument stored as a JsonElement of ValueKind == String — is exactly what
first-party IChatClient bridges produce, because they parse tool-call arguments into an
IDictionary<string, object?> / Dictionary<string, object?>, and System.Text.Json boxes every
value in such a dictionary as a JsonElement. So when a model double-encodes a tool argument, that
value arrives as a JsonElement{ValueKind: String} and hits the throwing arm above — the #6572
tolerance (raw string only) never runs.
Two public first-party bridges do exactly this today:
-
Microsoft.Extensions.AI.OpenAI —
OpenAIResponsesChatClientmaps aFunctionCallResponseItem
viaOpenAIClientExtensions.ParseCallContent(...), which calls
FunctionCallContent.CreateFromParsedArguments(json, ..., json => JsonSerializer.Deserialize(json, OpenAIJsonContext.Default.IDictionaryStringObject)).
Target typeIDictionary<string, object>⇒ values areJsonElement.
https://github.com/dotnet/extensions/blob/main/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
(thecase FunctionCallResponseItem:arm) -
Official Anthropic .NET SDK —
AnthropicClientExtensionsmaps aToolUseBlockvia
FunctionCallContent.CreateFromParsedArguments(element.GetRawText(), ..., json => JsonSerializer.Deserialize(json, ...GetTypeInfo(typeof(Dictionary<string, object?>)))).
Target typeDictionary<string, object?>⇒ values areJsonElement.
https://github.com/anthropics/anthropic-sdk-csharp/blob/main/src/Anthropic/AnthropicClientExtensions.cs
(thecase ToolUseBlock toolUse:arm inContentBlockValueToAIContent)
In both, a double-encoded argument reaches AIFunctionFactory's marshaller as a
JsonElement{ValueKind: String} and throws. Because both first-party bridges share this shape, a fix
in GetParameterMarshaller repairs every provider at once. Note the divergence with Semantic Kernel
(the original #6572 reporter): SK's KernelArguments carry raw .NET string values — the one shape
#6572 already tolerates — so the same logical defect was fixed for SK but not for the
JsonElement-string shape the OpenAI/Anthropic bridges produce.
Suggested fix
In the JsonElement arm of GetParameterMarshaller, when element.ValueKind == JsonValueKind.String
and the target type is not string, attempt to re-parse element.GetString() as JSON before
deserializing — mirroring the existing IsPotentiallyJson / MarshallViaJsonRoundtrip handling used
for raw strings.
- Dominant language
- C#
- Stars
- 3.2k
- Forks
- 895
- Avg merge
- 1d 10h
- Merged PRs (30d)
- 18
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from dotnet/extensions
-
area-telemetry untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
dotnet/extensions#7757 ·
-
bug untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
dotnet/extensions#7740 ·
-
untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
dotnet/extensions#7714 ·
-
bug untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
dotnet/extensions#7618 · 1 comment ·
-
bug needs-area-label untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
dotnet/extensions#7415 · 1 comment ·
All issues in dotnet/extensions
Similar issues
-
type/automation type/tech-debt
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
t/bug
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
ci-failure-cause test-failure
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
area:auth FE mvp P3
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
klasolsson81/jobbliggaren#1788 ·