Function calling with Claude on Amazon Bedrock in .NET Core

To call Claude with function calling (tool use) on Amazon Bedrock in .NET Core, you must set the InputSchema JSON type field to "object" at the top level of the schema. The error The value at toolConfig.tools.0.toolSpec.inputSchema.json.type must be one of the following: object occurs when the JSON object you pass to ToolInputSchema.Json has a type field that is not exactly "object". The fix is to ensure your anonymous type or dictionary includes Type = "object" as a top-level property, and that the Properties dictionary is correctly structured with nested objects for each property definition. The working code example below resolves this issue and demonstrates a complete function calling flow with Claude on Bedrock in .NET Core.
The Full Answer

This guide covers how to implement function calling (tool use) with Claude on Amazon Bedrock using .NET Core and the official AWSSDK.BedrockRuntime NuGet package (version 3.7.412.3 or later). The core challenge is correctly constructing the ToolInputSchema JSON so that the Bedrock API accepts it. We will walk through the complete setup, including the corrected code, prerequisites, and step-by-step explanation.
Prerequisites
Before you begin, ensure you have:
- An AWS account with access to Amazon Bedrock. You must have requested access to the Claude model (e.g.,
anthropic.claude-3-sonnet-20240229-v1:0) in the AWS console. - AWS credentials configured on your development machine (via environment variables, AWS CLI, or IAM roles).
- .NET Core SDK (version 6.0 or later) installed.
- The
AWSSDK.BedrockRuntimeNuGet package installed in your project. You can add it via the .NET CLI:
dotnet add package AWSSDK.BedrockRuntime --version 3.7.412.3
Or via the Package Manager Console:
Install-Package AWSSDK.BedrockRuntime -Version 3.7.412.3
Step-by-Step Implementation
Step 1: Set up the Bedrock client and message list
Create an instance of the AmazonBedrockRuntimeClient and prepare the conversation messages. The example below starts with a user message asking for the most popular song on a radio station.
using Amazon.BedrockRuntime;
using Amazon.BedrockRuntime.Model;
using Amazon.Runtime.Documents;
var bedrock = new AmazonBedrockRuntimeClient(Amazon.RegionEndpoint.USWest2); // Use your region
var messages = new List<Message>
{
new Message
{
Role = ConversationRole.User,
Content = new List<ContentBlock>
{
new ContentBlock { Text = "Get the most popular song played on a radio station." }
}
}
};
Explanation: The AmazonBedrockRuntimeClient is the entry point for all Bedrock API calls. The Messages list holds the conversation history. Each message has a Role (User or Assistant) and Content blocks. For a simple function call request, we start with a user message that describes the task.
Step 2: Define the tool configuration with correct JSON schema
The critical part is the ToolConfiguration object. The error from the original code occurs because the InputSchema.Json object must have a top-level type field set to "object". The Properties dictionary must contain nested objects for each property, each with its own type and description. The Required array lists property names that are mandatory.
Here is the corrected tool configuration:
var toolConfig = new ToolConfiguration
{
Tools = new List<Tool>
{
new Tool
{
ToolSpec = new ToolSpecification
{
Name = "GetSong",
Description = "Gets the current song on the radio",
InputSchema = new ToolInputSchema
{
Json = Document.FromObject(new
{
Type = "object", // Must be exactly "object"
Properties = new Dictionary<string, object>
{
{
"sign", new
{
Type = "string",
Description = "The call sign for the radio station for which you want the most popular song. Example calls signs are WZPZ and WKRP."
}
}
},
Required = new string[] { "sign" }
})
}
}
}
}
};
Key points:
- The
Typeproperty at the top level of the anonymous object is set to"object". This satisfies the Bedrock API requirement that the input schema must be a JSON object type. - The
Propertiesdictionary usesstringkeys (property names) andobjectvalues (each property's schema). Each property schema must itself be an object withTypeandDescription. - The
Requiredarray lists property names that Claude must provide when it decides to call the tool. Document.FromObject()converts the anonymous object into theAmazon.Runtime.Documents.Documenttype that the SDK expects.
Step 3: Make the Converse API call
Now, send the request to Bedrock:
var response = await bedrock.ConverseAsync(new ConverseRequest
{
ModelId = "anthropic.claude-3-sonnet-20240229-v1:0",
Messages = messages,
ToolConfig = toolConfig
});
Explanation: The ConverseAsync method sends the conversation and tool configuration to the specified model. The response will contain either a text response or a ToolUse content block if Claude decides to call the GetSong tool.
Step 4: Handle the response
After the API call, you need to process the response. Claude may respond with a ToolUse content block, which includes the tool name and input parameters. Here is how to handle it:
if (response.Output.Message.Content.Any(c => c.ToolUse != null))
{
var toolUse = response.Output.Message.Content.First(c => c.ToolUse != null);
Console.WriteLine($"Tool called: {toolUse.ToolUse.Name}");
Console.WriteLine($"Input: {toolUse.ToolUse.Input}");
// Extract the sign parameter
var sign = toolUse.ToolUse.Input.ToString(); // You may need to parse this JSON
Console.WriteLine($"Sign requested: {sign}");
// Here you would call your actual function (e.g., a database query or API)
// and then send the result back to Claude in a follow-up message.
}
else
{
// Claude responded with text
var text = response.Output.Message.Content.First(c => c.Text != null).Text;
Console.WriteLine($"Claude says: {text}");
}
Explanation: The response.Output.Message.Content list contains ContentBlock objects. Each block can be of type Text, ToolUse, or ToolResult. When Claude decides to use a tool, it returns a ToolUse block with the Name and Input (a JSON document containing the parameters). You then execute the actual function (e.g., query a database) and return the result in a subsequent message with a ToolResult block.
Complete Working Example
Here is the full corrected code that runs without the validation exception:
using Amazon.BedrockRuntime;
using Amazon.BedrockRuntime.Model;
using Amazon.Runtime.Documents;
var bedrock = new AmazonBedrockRuntimeClient(Amazon.RegionEndpoint.USWest2);
var messages = new List<Message>
{
new Message
{
Role = ConversationRole.User,
Content = new List<ContentBlock>
{
new ContentBlock { Text = "Get the most popular song played on a radio station." }
}
}
};
var toolConfig = new ToolConfiguration
{
Tools = new List<Tool>
{
new Tool
{
ToolSpec = new ToolSpecification
{
Name = "GetSong",
Description = "Gets the current song on the radio",
InputSchema = new ToolInputSchema
{
Json = Document.FromObject(new
{
Type = "object",
Properties = new Dictionary<string, object>
{
{
"sign", new
{
Type = "string",
Description = "The call sign for the radio station for which you want the most popular song. Example calls signs are WZPZ and WKRP."
}
}
},
Required = new string[] { "sign" }
})
}
}
}
}
};
var response = await bedrock.ConverseAsync(new ConverseRequest
{
ModelId = "anthropic.claude-3-sonnet-20240229-v1:0",
Messages = messages,
ToolConfig = toolConfig
});
// Process the response
if (response.Output.Message.Content.Any(c => c.ToolUse != null))
{
var toolUse = response.Output.Message.Content.First(c => c.ToolUse != null);
Console.WriteLine($"Tool called: {toolUse.ToolUse.Name}");
Console.WriteLine($"Input: {toolUse.ToolUse.Input}");
}
else
{
var text = response.Output.Message.Content.First(c => c.Text != null).Text;
Console.WriteLine($"Claude says: {text}");
}
When to Use This Approach
This approach is the standard way to implement function calling with Claude on Amazon Bedrock in .NET Core. Use it when:
- You need Claude to call external functions or APIs based on user input.
- You are building a conversational agent that can perform actions like database queries, API calls, or data processing.
- You want to use the
ConverseAPI for a simplified interface compared to the lower-levelInvokeModelAPI.
If you need more control over the model parameters (e.g., temperature, top_p), you can use the InvokeModel or InvokeModelWithResponseStream APIs instead, but the Converse API is recommended for most use cases because it handles message formatting and tool configuration more cleanly.
Common Pitfalls
1. Missing or incorrect type field in InputSchema
The most common mistake, as seen in the original Stack Overflow question, is not setting the top-level type to "object". The Bedrock API strictly validates that the input schema is a JSON object. If you omit Type = "object" or set it to something else (like "string"), you will get the ValidationException with the message The value at toolConfig.tools.0.toolSpec.inputSchema.json.type must be one of the following: object.
Fix: Always include Type = "object" at the top level of the anonymous object passed to Document.FromObject().
2. Incorrectly structured Properties dictionary
Each property in the Properties dictionary must itself be an object with Type and Description fields. If you use a simple string or a different structure, the API may reject the request or Claude may not understand the schema.
Fix: Use nested anonymous objects or Dictionary<string, object> for each property, as shown in the example.
3. Using the wrong model ID
The model ID must be exactly as specified by AWS. For Claude 3 Sonnet, the ID is "anthropic.claude-3-sonnet-20240229-v1:0". Using an incorrect or outdated model ID will result in a ResourceNotFoundException or ValidationException.
Fix: Verify the model ID in the AWS Bedrock console or documentation. The model ID format is anthropic.claude-<version>-<date>-v1:0.
4. Not handling the ToolUse response correctly
After Claude returns a ToolUse content block, you must execute the function and return the result in a follow-up message. If you do not handle this correctly, the conversation will stall or Claude will not receive the function output.
Fix: Always check for ToolUse blocks in the response. If present, execute the corresponding function and send a new message with a ToolResult content block.
5. Region mismatch
If your Bedrock client is configured for a region where the Claude model is not available, you will get an error. For example, us-west-2 (Oregon) supports Claude 3 Sonnet, but other regions may not.
Fix: Use a region that supports the model. Check the AWS documentation for model availability by region.
6. Missing AWS credentials
If your AWS credentials are not configured, the SDK will throw an AmazonServiceException with a message about missing credentials.
Fix: Configure credentials via environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN), the AWS CLI (aws configure), or an IAM role if running on EC2.
Related Questions
How do I return a tool result to Claude after executing a function?
After Claude calls a tool, you must send a follow-up message with a ToolResult content block. The ToolResult block contains the ToolUseId (from the ToolUse block) and the result content (usually a JSON string). Add this message to the messages list and call ConverseAsync again. This allows Claude to incorporate the function output into its response.
Can I use multiple tools in a single request?
Yes, you can define multiple tools in the Tools list of the ToolConfiguration. Each tool must have a unique Name and its own InputSchema. Claude will choose which tool to call based on the user's request and the tool descriptions. The Converse API supports up to 100 tools in a single request, though practical limits may vary.
What is the difference between Converse and InvokeModel for function calling?
The Converse API is a higher-level abstraction that handles message formatting, tool configuration, and response parsing automatically. The InvokeModel API gives you more control over the raw request body (including model parameters like temperature) but requires you to manually construct the JSON payload. For most function calling use cases, Converse is simpler and recommended. Use InvokeModel if you need streaming responses or custom inference parameters not exposed by Converse.
How do I handle streaming responses with function calling?
To stream responses, use the ConverseStreamAsync method instead of ConverseAsync. It returns a ConverseStreamResponse that contains a Stream property with events. You can listen for ContentBlockDeltaEvent for text chunks and ContentBlockStartEvent for tool use starts. Streaming is useful for real-time applications where you want to display partial results as Claude generates them.
The #1 Claude Newsletter
The most important claude updates, guides, and fixes โ one weekly email.
No spam, unsubscribe anytime. Privacy policy
Sources & References
This page was researched from 2 independent sources, combined and verified for completeness.
- 1.Anthropic Documentation โ QuickstartOfficial documentation ยท primary source
- 2.
Related Answers
Keep exploring Claude
Claude resources
Latest error solutions
Skip the manual work
Ready-made AI workflows and automation templates โ import and run instead of building from scratch.