Skip to content

Conversation

@mattbrailsford
Copy link

Summary

When using IChatClient with AIFunction tools, the InputSchema was incorrectly constructed by passing the properties dictionary to the constructor as rawData. This caused property definitions to be serialized at the top level of the JSON schema object instead of being nested under the "properties" key.

Before (invalid schema):

{
  "format": { "type": "string", "description": "..." },
  "type": "object"
}

After (valid schema):

{
  "type": "object",
  "properties": {
    "format": { "type": "string", "description": "..." }
  }
}

This resulted in Anthropic API validation errors:

tools.0.custom.input_schema: JSON schema is invalid. It must match JSON Schema draft 2020-12

Fix

Changed from constructor-based initialization to property initializer syntax in both:

  • AnthropicClientExtensions.cs
  • AnthropicBetaClientExtensions.cs
- InputSchema = new(properties) { Required = required },
+ InputSchema = new InputSchema() { Properties = properties, Required = required },

Fixes #82

* "Claude PR Assistant workflow"

* "Claude Code Review workflow"
@mattbrailsford mattbrailsford requested a review from a team as a code owner December 27, 2025 12:04
@PederHP
Copy link
Contributor

PederHP commented Dec 27, 2025

Have you tested this with the AIFunction tools created by AIFunctionFactory (and thus indirectly the MCP SDK)? Just to make sure it doesn't break those?

@mattbrailsford mattbrailsford force-pushed the fix/ichatclient-inputschema-properties branch from 17d974e to f300e7b Compare December 27, 2025 12:57
@mattbrailsford
Copy link
Author

@PederHP Yes - I've now validated with AIFunctionFactory tools. The existing tests in AnthropicClientBetaExtensionsTests.cs that use AIFunctionFactory.Create() were actually asserting the buggy schema format (properties at top level instead of nested under "properties").

I've updated those tests to expect the correct JSON Schema format:

- "input_schema": {
-     "query": {
-         "type": "string"
-     },
-     "type": "object",
-     "required": ["query"]
- }
+ "input_schema": {
+     "type": "object",
+     "properties": {
+         "query": {
+             "type": "string"
+         }
+     },
+     "required": ["query"]
+ }

All AIFunction related tests now pass with the fix. The schema that AIFunctionFactory.Create() produces has a "properties" key, and the SDK code correctly extracts those properties - it just wasn't nesting them correctly when building the InputSchema for the Anthropic API.

@PederHP
Copy link
Contributor

PederHP commented Dec 27, 2025

@PederHP Yes - I've now validated with AIFunctionFactory tools. The existing tests in AnthropicClientBetaExtensionsTests.cs that use AIFunctionFactory.Create() were actually asserting the buggy schema format (properties at top level instead of nested under "properties").

I've updated those tests to expect the correct JSON Schema format:

- "input_schema": {
-     "query": {
-         "type": "string"
-     },
-     "type": "object",
-     "required": ["query"]
- }
+ "input_schema": {
+     "type": "object",
+     "properties": {
+         "query": {
+             "type": "string"
+         }
+     },
+     "required": ["query"]
+ }

All AIFunction related tests now pass with the fix. The schema that AIFunctionFactory.Create() produces has a "properties" key, and the SDK code correctly extracts those properties - it just wasn't nesting them correctly when building the InputSchema for the Anthropic API.

But that's strange, because AIFunctions created by AIFunctionFactory.Create currently work on the next and main branches? I have production code using these that is working (unless they use nullable discriminated union types, but that's a different issue and there's an open PR for that).

I am worried that AIFunctionFactory uses a different top level schema internally than the SDK and that's why this was working as it was - and that this fix will break the extensions when AIFunction objects not created manually are passed.

Have you tried testing with a minimal reproducible console app that uses AIFunctionFactory.Create() and not just the tests? I can see the internal logic in your change, but it assumes that MEAI AIFunction has the same convention for input schema as the Anthropic API and I'm not sure it does.

@stephentoub

@mattbrailsford
Copy link
Author

@PederHP I've created a minimal test to verify what AIFunctionFactory.Create produces. Here's the output:

Function: test_function
JsonSchema:
{
  "type": "object",
  "properties": {
    "query": {
      "type": "string"
    }
  },
  "required": [
    "query"
  ]
}

So AIFunctionFactory produces a schema with properties correctly nested under "properties". The SDK code correctly reads this format (see lines 803-815 in AnthropicClientExtensions.cs):

if (inputSchema.TryGetProperty("properties", out JsonElement propsElement))
{
    foreach (JsonProperty p in propsElement.EnumerateObject())
    {
        properties[p.Name] = p.Value;  // Extracts: { "query": { "type": "string" } }
    }
}

But the bug is in how it writes to InputSchema. Before the fix:

InputSchema = new(properties) { Required = required }

This constructor puts properties as raw data at the top level, resulting in:

{ "query": { "type": "string" }, "type": "object", "required": ["query"] }

After the fix:

InputSchema = new InputSchema() { Properties = properties, Required = required }

This correctly produces:

{ "type": "object", "properties": { "query": { "type": "string" } }, "required": ["query"] }

As for why your code might be working - are you perhaps:

  1. Using the native Anthropic client directly (not via IChatClient)?
  2. Using tools without parameters?
  3. Using a different version of the SDK?

The bug specifically affects the IChatClient extension path when converting AIFunctionDeclaration tools to Anthropic's format.

@mattbrailsford
Copy link
Author

Here's the test code I used (with Microsoft.Extensions.AI v10.0.0):

using System.Text.Json;
using Microsoft.Extensions.AI;

var func1 = AIFunctionFactory.Create(
    (string query) => "result",
    "test_function",
    "A test function"
);

var func2 = AIFunctionFactory.Create(
    (string location, string unit) => $"Weather in {location}",
    "get_weather",
    "Gets the weather for a location"
);

var func3 = AIFunctionFactory.Create(
    () => DateTime.Now.ToString(),
    "get_time",
    "Gets the current time"
);

Console.WriteLine("=== AIFunctionFactory JsonSchema Output ===\n");

foreach (var func in new[] { func1, func2, func3 })
{
    Console.WriteLine($"Function: {func.Name}");
    Console.WriteLine($"JsonSchema:\n{JsonSerializer.Serialize(func.JsonSchema, new JsonSerializerOptions { WriteIndented = true })}\n");
    
    var hasProperties = func.JsonSchema.TryGetProperty("properties", out var props);
    Console.WriteLine($"Has 'properties' key: {hasProperties}");
    if (hasProperties)
    {
        Console.WriteLine($"Properties content: {props}\n");
    }
}

@PederHP
Copy link
Contributor

PederHP commented Dec 27, 2025

As for why your code might be working - are you perhaps:

  1. Using the native Anthropic client directly (not via IChatClient)?
  2. Using tools without parameters?
  3. Using a different version of the SDK?

The bug specifically affects the IChatClient extension path when converting AIFunctionDeclaration tools to Anthropic's format.

I am using IChatClient and the next branch, so that's puzzling. All my tools are from the MCP SDK, though, but that also uses AIFunctionFactory.

(You should rebase this fix onto next, as they don't accept PRs onto main except for releases).

@mattbrailsford mattbrailsford force-pushed the fix/ichatclient-inputschema-properties branch from f300e7b to 7d2b4bc Compare December 27, 2025 14:13
@mattbrailsford mattbrailsford changed the base branch from main to next December 27, 2025 14:13
@mattbrailsford
Copy link
Author

Thanks for the heads up about targeting next - I've rebased the PR onto upstream/next and updated the base branch.

I've re-validated everything against the next branch:

  • SDK builds successfully
  • All AIFunction-related unit tests pass
  • The standalone AIFunctionFactory test still confirms schemas have correctly nested "properties"

The same bug appears to exist in next for me at the same locations (line 844 in AnthropicClientExtensions.cs and line 1006 in AnthropicBetaClientExtensions.cs), though it's possible I'm missing something. Happy to make further adjustments if someone can point out what I'm missing.

…ions

The InputSchema constructor was being passed properties as rawData,
which caused them to be serialized at the top level of the JSON object
instead of under a "properties" key. This resulted in invalid JSON Schema
that failed Anthropic API validation with error:
"tools.0.custom.input_schema: JSON schema is invalid"

The fix uses property initializer syntax to correctly populate the
Properties property, ensuring the schema is valid JSON Schema draft 2020-12.

Also updates tests to expect the corrected schema format.

Fixes anthropics#82
@mattbrailsford mattbrailsford force-pushed the fix/ichatclient-inputschema-properties branch from 7d2b4bc to c227354 Compare December 27, 2025 14:24
@stephentoub
Copy link
Contributor

The InputSchema ctor used to accept "properties". That changed in 75a2cce#diff-79ec764df52aaa105c06a11788463cf56f9aa0f6fcfb87d49e433f4b78d1654bL274

@JanKrivanek
Copy link

This causes issues only when the schema for the tool conflicts with the recognized json schema top level keywords - e.g. pattern. So when you attempt to attach tool with pattern parameter - you'll get bad_request responces. Renaming it to something else (e.g. patternString) unblocks the calls.

I wasn't aware of this PR and created another - where I added unit test that valides the produced schema (so it fails without the fix):

https://github.com/anthropics/anthropic-sdk-csharp/pull/87/changes#diff-f961b25dbba036a751bc61874e7fdc0e15632c53e71e1cfff8ee7df4995cc2d5

@stephentoub
Copy link
Contributor

@sd-st could we get this small fix merged? Thanks!

@sd-st
Copy link
Collaborator

sd-st commented Jan 9, 2026

Looks like there's an extra commit in here, could we get rid of the one that adds claude code? It should already be in repo

@sd-st
Copy link
Collaborator

sd-st commented Jan 9, 2026

Other than that it LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] IChatClient tool conversion produces invalid JSON Schema - properties not nested correctly

6 participants