When the Anthropic Messages API returns HTTP 400 with type: invalid_request_error right after you attach a tools array, the request body is syntactically valid JSON but the input_schema inside each tool fails provider validation. Plain chat completions work; the same prompt with tools fails. That pattern exploded in 2026 as teams migrated OpenAI-style tool definitions into Claude backends without re-checking schema draft rules.
This article gives the fastest path to a 200 tool-use loop: shrink schemas to Anthropic’s supported subset, lint offline, and lock contracts per deployed model family.
Problem summary
Typical symptoms:
POST /v1/messagessucceeds withtoolsomitted.- Adding
toolstriggers 400 before any model tokens stream. - Error text mentions JSON schema, tool,
input_schema, or invalid_request. - You copied
parametersfrom an OpenAI function spec or a JSON Schema Draft 2020-12 file verbatim.
Exact wording teams report:
invalid_request_error: tools.0.custom.input_schema: …- “Works in OpenAI; Claude rejects the same schema.”
Why this spikes now (2026 tool-heavy NPC backends)
- Multi-provider NPC stacks now ship tool calling for inventory, quest state, and moderation—not chat-only prompts.
- Prompt migrations often paste OpenAI
parametersinto Anthropicinput_schemawithout translation. - Strict JSON Schema features (
$ref,oneOf,nullable,additionalProperties: falseon deep trees) are common in auto-generated SDK stubs but not portable across providers. - 2026 partner questionnaires ask for documented tool contracts—teams discover 400s during compliance review, not local dev.
Root causes
- Unsupported keywords —
$ref,$defs,oneOf/anyOf/allOf,nullable,constcombinations Anthropic rejects. - Wrong nesting —
input_schemamust be a single JSON Schema object root withtype: "object"andproperties; arrays at root or barestringroots fail. - Duplicate tool names — two entries named
get_quest_statein one request. - Oversized schema — hundreds of properties or long enum lists exceed practical limits and fail validation.
- Draft drift — generator outputs Draft 2020-12; Anthropic expects a constrained dialect closer to classic JSON Schema for tools.
Contrast: Anthropic API 529 overloaded means the service is busy. 400 invalid_request means your payload is wrong—retries will not help until the schema changes.
Fastest safe fix (under 15 minutes)
Step 1 — Reproduce with a frozen curl payload
Save tools_repro.json and call the API without your game loop:
curl -sS https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d @payload.json
payload.json minimal shape:
{
"model": "claude-sonnet-4-20250514",
"max_tokens": 256,
"messages": [{ "role": "user", "content": "Call get_quest_state for player 1." }],
"tools": [
{
"name": "get_quest_state",
"description": "Return active quest id and step index.",
"input_schema": {
"type": "object",
"properties": {
"player_id": { "type": "string" }
},
"required": ["player_id"]
}
}
]
}
If this returns 200, binary-search which field in your production schema breaks validation.
Step 2 — Flatten unsupported constructs
| Remove or replace | Use instead |
|---|---|
$ref / $defs |
Inline one level of properties |
oneOf / anyOf |
Separate tools per variant, or string enum |
nullable: true |
type: ["string", "null"] only if accepted; prefer optional via required omission |
additionalProperties: false everywhere |
Top-level object only; drop on nested nodes if rejected |
format: uuid |
type: string + description “UUID” |
Example — before (often 400):
"input_schema": {
"$ref": "#/definitions/QuestQuery"
}
After (portable):
"input_schema": {
"type": "object",
"properties": {
"quest_id": { "type": "string", "description": "Stable quest slug" },
"include_hidden": { "type": "boolean" }
},
"required": ["quest_id"]
}
Step 3 — Split mega-tools into focused tools
Instead of one game_action tool with twenty optional fields, ship:
get_quest_stateset_quest_flaggrant_item
Each schema stays under ~15 properties. Models pick tools more reliably and validation passes.
Step 4 — Lint offline before CI deploy
Add a repo script that:
- Loads committed
tools/*.json. - Asserts root
type === "object". - Rejects forbidden keys (
$ref,oneOf, …). - Fails CI on duplicate
name.
Optional: validate against Anthropic’s documented tool schema notes for your API version in Anthropic API errors and tool-use guides.
Step 5 — Contract-test per model family
Pin tests that POST a minimal tool-use message to staging on every deploy:
expect HTTP 200
expect first tool_use block name == get_quest_state
When Anthropic ships stricter validation on a new model id, the test fails in staging—not on player traffic.
Verification checklist
- [ ] curl replay with production
toolsarray returns 200 - [ ] Game backend logs show
tool_use/tool_resultround-trip - [ ] Removing
toolswas only used to confirm diagnosis—not left as production path - [ ] Schema files have no
$reforoneOf - [ ] Each tool
nameis unique andsnake_case - [ ] CI contract test passes on staging key
Alternative fixes
A — Generate schemas from code types (narrow)
Derive input_schema from a small hand-written struct (Rust schemars with feature flags off for refs, C# source generator with flat DTOs). Do not dump full OpenAPI components.
B — Two-step dialogue without tools
If migration deadline is tonight: keep Claude on plain messages and pass quest state in the system block. Re-introduce tools after flattening—acceptable short-term, worse for long tool chains.
C — Chunk tools across requests
If you need many actions, rotate tool subsets per scene (combat_tools, shop_tools) instead of one request carrying twelve schemas.
Prevention
- Store Anthropic-flavored tool JSON separately from OpenAI
functions—never alias the same file. - Version tool specs (
tools_spec_version: 3) in your NPC config; bump when schema changes. - Require PR review when
tools/changes; run offline linter in pre-commit. - Document “allowed schema keywords” in
docs/ai_tool_contract.mdfor designers.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| 400 only on one model id | Stricter validation on newer Claude | Flatten further; test on target model |
| 400 after prompt migration only | Tools pasted from OpenAI export | Rebuild input_schema per Step 2 |
| Intermittent 400 | Sometimes duplicate tool name in dynamic assembly | Dedupe by name before POST |
| 400 with huge schema | Payload size / complexity | Split tools; trim enums |
| 200 in curl, 400 in game | Extra wrapper keys on tool object | Match API shape exactly (name, description, input_schema only) |
FAQ
Is 400 the same as 529?
No. 400 invalid_request is a client/schema bug. 529 overloaded needs backoff—see the 529 overload help.
Can I use strict: true OpenAI-style mode?
That flag is OpenAI-specific. Anthropic tool validation follows its own input_schema rules—translate, do not copy flags.
Should description mention JSON Schema keywords?
Use plain language for the model. Put structural rules in input_schema, not in description prose.
Do I need tools for every NPC line?
No. Use tools when the model must commit structured game actions. Flavor text can stay text-only.
Related links
- Anthropic API 529 Overloaded in Game Backend - Queue Retry and Fallback Model Fix — when requests are valid but capacity is full
- OpenAI API 429 Too Many Requests in Unity NPC Dialogue - Retry Backoff and Token Budget Fix — rate limits vs schema errors on another provider
- ElevenLabs Conversational AI Unity SDK 502 Bad Gateway - Retry and Fallback Fix — voice layer after dialogue tools succeed
- 15 Free LLM-Driven NPC Dialogue and Local Fallback Net Resources (2026) — multi-provider stack and fallback-net discipline
- Official: Anthropic API errors
Fix the schema, not the retry policy—400 on tools means Claude never saw your game logic, and no amount of backoff will change that.