Claude API Function Calling Schema Validation Guide
Claude's function calling (tool use) feature lets the model return structured JSON that maps to a function you define. The problem developers hit almost immediately: Claude generates arguments that are syntactically valid JSON but don't actually match your expected schema — a missing required field, a string where you expected a number, an extra property, or an enum value that doesn't exist in your list. Schema validation is the step between "Claude returned something" and "my code can safely execute this."
The short answer: define your tool input schema using JSON Schema, validate every tool_use response against it with a library like Zod, Ajv, or Pydantic before you execute anything, and return validation errors back to Claude as a tool_result so it can self-correct. Claude does not guarantee schema conformance just because you declared one — it's a strong prior, not a hard constraint — so validation is not optional if you're calling real functions with real side effects.
Why Schema Validation Matters for Tool Use
When you define a tool, you pass an input_schema describing the expected shape of the arguments:
{
"name": "get_weather",
"description": "Get current weather for a location",
"input_schema": {
"type": "object",
"properties": {
"location": { "type": "string" },
"unit": { "type": "string", "enum": ["celsius", "fahrenheit"] }
},
"required": ["location"]
}
}
This schema guides generation, but it's a prompt-level constraint, not a type system. Claude can still return {"location": "Berlin", "unit": "Kelvin"} or omit location entirely if the conversation context is ambiguous. If your backend passes that straight into a database query, a payment call, or a shell command, you're trusting unverified input from a model output — the same risk class as trusting unverified user input.
A Practical Validation Pipeline
The pattern that works reliably in production:
- Define the schema once, in a format usable both for the Claude tool declaration and for runtime validation (JSON Schema is ideal because most validators consume it directly).
- Parse the tool_use block from the response.
- Validate before execution. Reject or repair invalid input.
- On failure, send a tool_result with
is_error: truedescribing what was wrong, so Claude can retry with corrected arguments.
Here's a minimal Node.js example using Ajv:
import Ajv from "ajv";
const ajv = new Ajv();
const schema = {
type: "object",
properties: {
location: { type: "string" },
unit: { type: "string", enum: ["celsius", "fahrenheit"] }
},
required: ["location"],
additionalProperties: false
};
const validate = ajv.compile(schema);
function handleToolUse(toolUseBlock) {
const valid = validate(toolUseBlock.input);
if (!valid) {
return {
type: "tool_result",
tool_use_id: toolUseBlock.id,
content: `Invalid arguments: ${ajv.errorsText(validate.errors)}`,
is_error: true
};
}
return executeGetWeather(toolUseBlock.input);
}
The additionalProperties: false flag matters more than it looks — it's your defense against Claude inventing extra fields that silently get ignored by your function but could indicate a misunderstanding of the tool's purpose.
Using Zod or Pydantic Instead
If you're already using TypeScript, Zod gives you both compile-time types and runtime validation from one definition:
import { z } from "zod";
const WeatherArgs = z.object({
location: z.string().min(1),
unit: z.enum(["celsius", "fahrenheit"]).optional()
});
function handleToolUse(toolUseBlock) {
const result = WeatherArgs.safeParse(toolUseBlock.input);
if (!result.success) {
return buildErrorResult(toolUseBlock.id, result.error.message);
}
return executeGetWeather(result.data);
}
In Python, Pydantic models serve the same purpose and can even generate the JSON Schema you pass to Claude's input_schema, keeping the two in sync automatically instead of maintaining duplicate schema definitions by hand.
Common Schema Mismatches to Guard Against
- Type coercion gaps: Claude sometimes returns
"42"instead of42for numeric fields. Decide whether to coerce or reject — coercion is more forgiving but can mask real bugs. - Enum drift: if you update allowed values in your business logic but not in the tool definition, Claude will keep generating the old values.
- Nested object depth: deeply nested schemas are where Claude is most likely to flatten or restructure fields incorrectly. Keep tool schemas as shallow as practical.
- Optional vs required confusion: mark fields required only when your function truly cannot run without them — over-marking fields as required increases retry loops.
- Multiple tool calls in one turn: when Claude calls several tools in parallel, validate each tool_use block independently; one failure shouldn't block the others from executing.
Retrying Gracefully
Returning a clear, specific error message in the tool_result block is what lets Claude actually fix the problem instead of repeating the same mistake. Vague messages like "invalid input" produce vague retries. Messages like "unit must be one of: celsius, fahrenheit — received 'Kelvin'" give the model exactly what it needs to correct course on the next turn. For details on structuring these conversation turns, see /docs/tools.
Where SubToAPI Fits
If you're already managing Claude access across a team and want tool-use traffic flowing through a single, observable endpoint, SubToAPI turns your Claude access into a standard HTTPS API with per-application keys (sub_live_...), streaming, and usage metadata — so you can see which app or key is generating malformed tool calls without digging through raw logs. Check /docs/tools for request formats, or /docs/quickstart to get a key issued in minutes. Plans start with a free trial at /signup, with pricing details at /pricing.
FAQ
Does Claude guarantee its function call arguments match my input_schema? No. The schema strongly influences generation but isn't enforced like a type system. Always validate tool_use input at runtime before executing any function with real side effects.
What's the best library for validating Claude tool call arguments? Ajv or Zod for JavaScript/TypeScript, Pydantic for Python. All three can derive from or generate JSON Schema, so you can share one schema definition between the Claude tool declaration and your validator.
What should I do when validation fails? Return a tool_result block with is_error: true and a specific, actionable error message. This lets Claude retry with corrected arguments on the next turn instead of failing silently or crashing your backend.