Fix Vercel AI SDK JSON Schema Errors with Anthropic (2026)
Getting 400 errors from Anthropic when using Vercel AI SDK structured output? Unsupported JSON Schema keywords like exclusiveMinimum, minItems, and not are silently breaking your schema. Here's the fix.
Have broken JSON right now? Fix it free in under 1 second — no signup.
Fix My JSON →Fix Vercel AI SDK JSON Schema Errors with Anthropic
If you're using the Vercel AI SDK with Anthropic's Claude for structured output and seeing 400 errors, the cause is almost always unsupported JSON Schema keywords being sent to the Anthropic API.
The Vercel AI SDK generates JSON schemas from your Zod or object schemas. Not all JSON Schema keywords are supported by all providers. Keywords that OpenAI accepts, Anthropic rejects. Keywords that pass locally, fail in production. The SDK doesn't always strip these before sending.
This is an active issue in the vercel/ai repository (see #13355 and #14342). Here's what's happening and how to fix it today.
The Root Cause: Provider JSON Schema Subsets
Anthropic's structured output (tool use mode) accepts a subset of JSON Schema. It explicitly rejects:
exclusiveMinimum(useminimumwith manual offset instead)exclusiveMaximum(same)minItems/maxItemson arrays (useminimumon a.lengthfield instead)notif/then/elseconditional schemascontainson arrayspatternPropertiesunevaluatedProperties
OpenAI's structured output also has restrictions, but a different subset. When you use the Vercel AI SDK with a Zod schema that generates any of these keywords, the SDK may pass them through to whichever provider you're using — resulting in a 400 error from Anthropic.
Diagnosing the Problem
The first sign is a 400 error from the Anthropic API with a message like:
400 {"type":"error","error":{"type":"invalid_request_error","message":"tools.0.input_schema: unsupported keyword 'exclusiveMinimum'"}}
Or more cryptically:
Error: 400 Bad Request
To diagnose, log the exact schema being sent:
import { generateObject } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
const schema = z.object({
score: z.number().min(0).max(100),
items: z.array(z.string()).min(1).max(10),
});
// Log what schema the SDK generates
console.log(JSON.stringify(schema._def, null, 2));
// Or use zodToJsonSchema from zod-to-json-schema
import { zodToJsonSchema } from "zod-to-json-schema";
console.log(JSON.stringify(zodToJsonSchema(schema), null, 2));
Common offenders in the generated schema:
{
"score": {
"type": "number",
"minimum": 0,
"maximum": 100
},
"items": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"maxItems": 10
}
}
minItems/maxItems — rejected by Anthropic.
Fix 1: Remove Constraints from the Zod Schema
The simplest fix: remove constraints Anthropic doesn't support and validate them yourself after receiving the response.
// Before — generates minItems/maxItems
const schema = z.object({
score: z.number().min(0).max(100),
items: z.array(z.string()).min(1).max(10),
});
// After — Anthropic-safe schema
const schema = z.object({
score: z.number(), // validate range in Inspection Layer
items: z.array(z.string()), // validate length in Inspection Layer
});
// Then validate after receiving response
const result = await generateObject({ model, schema, prompt });
if (result.object.score < 0 || result.object.score > 100) {
throw new Error("Score out of range");
}
if (result.object.items.length < 1 || result.object.items.length > 10) {
throw new Error("Items count invalid");
}
This is the cleanest approach. Your Zod schema defines structure; your application code validates constraints. See the LLM Inspection Layer guide for a full treatment of this pattern.
Fix 2: Strip Unsupported Keywords Before Sending
If you need to keep constraints in your Zod schema (for documentation and non-AI validation), strip incompatible keywords before passing the schema to the AI SDK:
import { zodToJsonSchema } from "zod-to-json-schema";
function stripAnthropicUnsupportedKeywords(schema: object): object {
const UNSUPPORTED = [
"exclusiveMinimum",
"exclusiveMaximum",
"minItems",
"maxItems",
"minProperties",
"maxProperties",
"not",
"if",
"then",
"else",
"contains",
"patternProperties",
"unevaluatedProperties",
"unevaluatedItems",
];
if (typeof schema !== "object" || schema === null) return schema;
const cleaned: Record<string, unknown> = {};
for (const [key, value] of Object.entries(schema)) {
if (UNSUPPORTED.includes(key)) continue;
if (typeof value === "object" && value !== null) {
cleaned[key] = stripAnthropicUnsupportedKeywords(value as object);
} else {
cleaned[key] = value;
}
}
return cleaned;
}
// Usage with generateObject
const rawSchema = zodToJsonSchema(myZodSchema);
const anthropicSafeSchema = stripAnthropicUnsupportedKeywords(rawSchema);
Then pass anthropicSafeSchema directly as the JSON schema in your API call instead of using the Zod schema through the SDK.
Fix 3: Use a Provider-Compatible Zod Subset
Avoid generating unsupported keywords by using only Zod methods that map to Anthropic-safe JSON Schema:
| Zod method | Generated keyword | Anthropic support |
|---|---|---|
.string() | type: string | ✅ |
.number() | type: number | ✅ |
.boolean() | type: boolean | ✅ |
.array() | type: array, items: ... | ✅ |
.object() | type: object, properties: ... | ✅ |
.optional() | removes from required | ✅ |
.nullable() | anyOf with null | ✅ |
.enum() | enum: [...] | ✅ |
.literal() | const: value | ✅ |
.min(n) on string | minLength: n | ✅ |
.max(n) on string | maxLength: n | ✅ |
.min(n) on number | minimum: n | ✅ |
.max(n) on number | maximum: n | ✅ |
.min(n) on array | minItems: n | ❌ |
.max(n) on array | maxItems: n | ❌ |
.gt(n) on number | exclusiveMinimum: n | ❌ |
.lt(n) on number | exclusiveMaximum: n | ❌ |
Design your Zod schemas to stay in the ✅ column when targeting Anthropic.
Fix 4: Use the Anthropic SDK Directly
For full control over the schema sent to Anthropic, bypass the Vercel AI SDK abstraction and use the Anthropic SDK directly:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
tools: [
{
name: "extract_data",
description: "Extract structured data from the input",
input_schema: {
type: "object",
properties: {
score: {
type: "number",
description: "Score between 0 and 100",
},
items: {
type: "array",
items: { type: "string" },
description: "List of items (1-10)",
},
},
required: ["score", "items"],
},
},
],
tool_choice: { type: "tool", name: "extract_data" },
messages: [{ role: "user", content: prompt }],
});
const toolUse = response.content.find((c) => c.type === "tool_use");
const data = toolUse?.input as { score: number; items: string[] };
// Validate constraints yourself
if (data.score < 0 || data.score > 100) throw new Error("Score out of range");
if (data.items.length < 1) throw new Error("Need at least 1 item");
This is more verbose but gives you precise control over exactly what schema Anthropic receives.
The Cross-Provider Problem
The underlying issue is that different providers support different subsets of JSON Schema:
| Keyword | OpenAI strict | Anthropic | Gemini |
|---|---|---|---|
minimum / maximum | ✅ | ✅ | ✅ |
exclusiveMinimum | ✅ | ❌ | Partial |
minItems / maxItems | ✅ | ❌ | ✅ |
not | ❌ | ❌ | ❌ |
if/then/else | ❌ | ❌ | ❌ |
$ref | ✅ | ❌ | ❌ |
anyOf | ✅ | ✅ | ✅ |
oneOf | ❌ | ✅ | ❌ |
If you're building a multi-provider system, use only the intersection — keywords that all providers support. See the cross-provider JSON Schema compatibility guide for the full safe subset.
Debugging JSON Schema Errors Fast
When you hit a 400 error:
- Log the full error response — the message usually names the problematic keyword
- Log the schema being sent — use
zodToJsonSchemaor SDK debug mode - Strip one keyword at a time — binary search to find the culprit
- Test with curl — send the schema directly to the Anthropic API to isolate the SDK
Quick curl test:
curl 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 '{
"model": "claude-sonnet-4-6",
"max_tokens": 100,
"tools": [{
"name": "test",
"input_schema": YOUR_SCHEMA_HERE,
"description": "test"
}],
"tool_choice": {"type": "tool", "name": "test"},
"messages": [{"role": "user", "content": "test"}]
}'
If curl works but the SDK fails, the SDK is adding keywords. If curl fails, the schema itself has unsupported keywords.
For the JSON parsing side — if your structured output comes back malformed despite the schema, AI JSON Medic can repair LLM responses that failed to parse correctly.
Frequently Asked Questions
Why does this work with OpenAI but fail with Anthropic?OpenAI and Anthropic support different subsets of JSON Schema in their structured output implementations. A schema that is valid for OpenAI's response_format may be rejected by Anthropic's tools parameter. The Vercel AI SDK doesn't always normalize schemas per provider.
This is being tracked in vercel/ai issues #13355 and #14342. Until a fix ships, use the workarounds above. Check the current SDK version's changelog — provider-specific schema normalization may have shipped since this guide was written.
Is there a safe schema I can use across all providers?Yes — see the cross-provider JSON Schema compatibility guide. The safe subset uses type, properties, required, enum, anyOf, minimum/maximum, and minLength/maxLength only.
generateText with tool calls?
Yes — generateText with tools also sends JSON schemas for tool parameters. The same keyword restrictions apply. generateObject uses the structured output path, but tool calls in generateText hit the same schema validation.
streaming / streamObject APIs?
Same issue — the schema is validated before streaming starts. A 400 error will abort the stream before any tokens arrive.
Still dealing with broken JSON?
Paste it in and get it fixed in under 1 second — free, no signup, no install. Works with ChatGPT, Claude, n8n, and any AI output.
Fix My JSON Free →Related Articles