[Microsoft.Extensions.AI] AIFunctionFactory does not tolerate a JsonElement of ValueKind String as a JSON-stringified argument (gap in #6572)

Open Beginner friendly
#7,626 3 comments 0 reactions 0 assignees View on GitHub

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

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

area-ai enhancement

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.

https://github.com/dotnet/extensions/blob/v10.4.1/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs#L889

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:

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dotnet/extensions

All issues in dotnet/extensions

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.