AI Integration Problems

Anthropic Messages API 400 invalid_request_error for Tools JSON Schema After Prompt Migration - Fix

Fix Anthropic Claude Messages API 400 invalid_request_error when tool input_schema uses unsupported JSON Schema draft features after 2026 NPC prompt migrations.

By GamineAI Team

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/messages succeeds with tools omitted.
  • Adding tools triggers 400 before any model tokens stream.
  • Error text mentions JSON schema, tool, input_schema, or invalid_request.
  • You copied parameters from 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 parameters into Anthropic input_schema without translation.
  • Strict JSON Schema features ($ref, oneOf, nullable, additionalProperties: false on 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

  1. Unsupported keywords$ref, $defs, oneOf / anyOf / allOf, nullable, const combinations Anthropic rejects.
  2. Wrong nestinginput_schema must be a single JSON Schema object root with type: "object" and properties; arrays at root or bare string roots fail.
  3. Duplicate tool names — two entries named get_quest_state in one request.
  4. Oversized schema — hundreds of properties or long enum lists exceed practical limits and fail validation.
  5. 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_state
  • set_quest_flag
  • grant_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:

  1. Loads committed tools/*.json.
  2. Asserts root type === "object".
  3. Rejects forbidden keys ($ref, oneOf, …).
  4. 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 tools array returns 200
  • [ ] Game backend logs show tool_use / tool_result round-trip
  • [ ] Removing tools was only used to confirm diagnosis—not left as production path
  • [ ] Schema files have no $ref or oneOf
  • [ ] Each tool name is unique and snake_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.md for 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

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.