Блог • AI / Structured Output
Насколько хорош JSON у Claude Fable 5.1? Structured Output и план проверки API
Claude Fable 5.1 shipped on 2026-09-01. The API model name is claude-fable-5-1. The docs mention both output_config.format and strict: true on tools—both promise structured JSON, with different guarantees.
Claude Fable 5.1 makes Structured Output a GA Messages API feature: attach a JSON Schema and the final answer is constrained to those fields, types, and required keys. For API work the question is no longer “will it emit JSON?” but “how far does this contract hold on Claude vs OpenAI vs Gemini?”Structured Output is no longer a beta header.
This article covers:
- Where Fable 5.1 sits in the lineup—and when it is worth the cost
- What JSON outputs vs Strict tool use actually guarantee
- A copy-paste output_config.format request
- How to write Agents after forced tool use was removed
- A cross-vendor API test plan: what to measure and how to score it
Keep this in mind: Fable 5.1 is good enough for production contracts, but API-level conformance is not business correctness. Forced tools 400; minimum / minLength never reach the decoder. Below: specs → limits → vendor compare → test plan.
What is Claude Fable 5.1
Claude Fable 5.1 is Anthropic’s flagship for long-horizon agents, multistep research, and document/spreadsheet work. The model ID on the Claude API, Amazon Bedrock, Google Cloud, and Microsoft Foundry is claude-fable-5-1. Knowledge cutoff June 2026, 1M context, 128K max synchronous output.
Official guidance: start most workloads on Claude Opus 5. Reach for Fable 5.1 when Opus 5 at higher effort still fails evals, or the job is a long-running agent. Pricing matches that role: $10 / $50 per MTok—twice Opus 5.
| Spec | Claude Fable 5.1 | Claude Opus 5 | Claude Sonnet 5 |
|---|---|---|---|
| Context / max output | 1M / 128K | 1M / 128K | 1M / 128K |
| Price ($/MTok) | $10 / $50 | $5 / $25 | $2 / $10 |
| Thinking | Adaptive, always on | Adaptive | Adaptive |
| Default effort | high | high | high |
Official docs: Claude Fable 5.1, Structured outputs.
JSON / Structured Output limits
Fable 5.1 exposes two complementary paths—use either, or both in one request:
- JSON outputs: the final answer goes through
output_config.formatand comes back as a schema-valid JSON text block. Use it for extraction, reports, and downstream APIs. - Strict tool use: set
strict: trueon the tool so the name and arguments pass schema checks. Use it when an Agent calls external systems.
Versus “please return JSON” in the prompt, the difference is decoding: the schema constrains tokens instead of hoping json.loads works after the fact.
| Capability | Fable 5.1 behavior | What it means for APIs |
|---|---|---|
| Valid JSON | Guaranteed on the JSON outputs path | Fewer regex patches and second-pass LLM parses |
| Fields / types / required | Conforms to the schema you send | Safe to feed validators and business code |
| Numeric / length constraints | Unsupported—request returns 400 | Keep minimum and minLength in your app |
| Force a specific tool | tool_choice any/tool returns 400 | Use auto + strict, or switch to JSON outputs |
For the shared Structured Output model see 2026 AI Structured Output explained. This piece only covers Claude-specific gaps and how to measure them.
How to use output_config.format
In 2026 the beta field output_format moved to output_config.format. New code should not send the structured-outputs-2025-11-13 header. Python SDK 1.0+ raises TypeError if you pass the old field to messages.create().
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-fable-5-1",
max_tokens=1024,
messages=[
{
"role": "user",
"content": "Extract ticket fields from: billing failed for acct-9921, user wants a refund.",
}
],
output_config={
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "technical", "account", "other"],
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"],
},
"account_id": {"type": "string"},
"summary": {"type": "string"},
},
"required": ["category", "priority", "account_id", "summary"],
"additionalProperties": False,
},
}
},
)
print(next(block.text for block in response.content if block.type == "text"))
Objects must set additionalProperties: false; list every property in required. Miss one and the request may 400, or extra keys will not appear.
With Pydantic, client.messages.parse() still accepts output_format as a convenience name, rewrites it to output_config.format, adds additionalProperties: false, and strips unsupported constraints.
Strict tool use: do not force tools
Thinking is always on for Fable 5.1. A forced tool call would skip it and dump reasoning into arguments. That is why tool_choice: {"type": "any"} and {"type": "tool"} return 400 invalid_request_error. For schema-valid tool args, keep auto and turn on strict.
{
"model": "claude-fable-5-1",
"max_tokens": 1024,
"tool_choice": { "type": "auto" },
"tools": [
{
"name": "create_ticket",
"description": "Create a support ticket from extracted fields.",
"strict": true,
"input_schema": {
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "technical", "account", "other"]
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"]
},
"summary": { "type": "string" }
},
"required": ["category", "priority", "summary"],
"additionalProperties": false
}
}
],
"messages": [
{ "role": "user", "content": "Use create_ticket for this refund request." }
]
}
To make the model call a tool, say so in the prompt—e.g. “Use create_ticket for this refund.” Schema also shows up as MCP inputSchema / outputSchema; see Why AI Agents need JSON Schema.
vs OpenAI / Gemini
The same business schema mounts on three different fields. For a multi-model router, keep one schema and three thin adapters.
| Item | Claude Fable 5.1 | OpenAI | Gemini |
|---|---|---|---|
| Request field | output_config.format |
response_format.json_schema |
responseSchema |
| Type flag | json_schema |
json_schema + strict |
application/json |
| Extra fields | additionalProperties: false required | Same requirement in Strict mode | Recommended; errors are not identical |
| Tool contract | tools[].strict + input_schema | tools[].parameters / strict | function calling + responseSchema |
OpenAI-compatible APIs (e.g. DeepSeek V4-Pro) follow OpenAI field names—do not paste Claude’s output_config as-is.
API development test plan
“How good is the JSON?” is not one happy-path call. Use one schema and one case set against Claude / OpenAI / Gemini, then record four pass rates.
-
1
Freeze a cross-vendor schema
Stay in the shared subset: object, string, integer, enum, required, additionalProperties: false. Skip minimum and recursive $ref or you are testing vendor limits, not the model.
-
2
Collect 4–8 real texts
Cover enum edges, missing fields, mixed intent, and long descriptions. Label expected category / priority up front—do not relabel after the run.
-
3
Score four conformance checks
All required keys present, no extras, enum in the set, types match. All four must pass. Track json.loads failures separately—they should be near zero on Structured Output.
-
4
Test the breaking changes alone
Send tool_choice: any to Fable 5.1 and expect 400. Send a schema with minimum / minLength and expect 400. These are upgrade regressions.
-
5
Measure latency and tokens, not just accuracy
Default effort is high and thinking is always on, so the same schema is often slower and costlier than Sonnet 5 or Gemini Flash. Log P50 latency and output tokens before you put Fable on the hot path.
CASES = [
"billing failed for acct-9921, user wants a refund",
"cannot login after password reset, error 403",
"please upgrade our workspace to Enterprise",
"the app crashes when opening a 12MB JSON file",
]
EXPECTED_KEYS = {"category", "priority", "account_id", "summary"}
def score(payload: dict) -> dict:
keys = set(payload)
return {
"has_required": EXPECTED_KEYS <= keys,
"no_extra": keys <= EXPECTED_KEYS,
"enum_ok": payload.get("category") in {"billing", "technical", "account", "other"},
"types_ok": all(isinstance(payload.get(k), str) for k in EXPECTED_KEYS),
}
Paste the three raw JSON payloads into JSONNote Diff—field drift shows up faster than in logs. If pass rate drops below 95%, check for unsupported keywords first, then prompt vs enum conflicts.
Schema limits: what the API rejects
JSON outputs and Strict tool use share one JSON Schema subset. Unsupported features 400 immediately; the model will not “try its best.”
| Category | Supported | Not supported |
|---|---|---|
| Basics / numbers | type / properties / required / enum / const | minimum / maximum / multipleOf |
| String formats | date-time / email / uri / uuid | minLength / maxLength |
| Arrays | minItems = 0 or 1 | Larger minItems / maxItems |
| $ref | Local $ref / $def | External $ref, recursive schemas |
Python / TypeScript SDKs strip unsupported constraints into descriptions. Do not rely on that silent rewrite—eval and prod should send the same trimmed schema.
Validation workflow: API to business layer
Three layers: API JSON outputs or Strict tool → business json.loads + JSON Schema validate → monitor violation rate. Fable 5.1 does not replace the last two: empty content, truncation, and enum casing still happen.
import json
from jsonschema import validate, ValidationError
SCHEMA = {
"type": "object",
"properties": {
"category": {"type": "string", "enum": ["billing", "technical", "account", "other"]},
"priority": {"type": "string", "enum": ["low", "medium", "high"]},
"account_id": {"type": "string"},
"summary": {"type": "string"},
},
"required": ["category", "priority", "account_id", "summary"],
"additionalProperties": False,
}
def parse_llm_json(raw: str) -> dict:
try:
data = json.loads(raw)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON: {e}") from e
try:
validate(instance=data, schema=SCHEMA)
except ValidationError as e:
raise ValueError(f"Schema mismatch: {e.message}") from e
return data
Use the same SCHEMA for the request and the check. During development, paste the raw model text and schema into JSONNote—faster than print() in a terminal.
Debug model JSON with JSONNote
The slow part of a cross-vendor eval is comparing three payloads. JSONNote runs locally in the browser; keys and samples never upload:
-
1
Validate schema vs output
Paste Fable / OpenAI / Gemini JSON plus one schema into JSON Schema to see missing fields, wrong types, and extra keys.
-
2
Format the raw response
Use JSON Formatter to confirm valid JSON and rule out log-escaping false positives.
-
3
Diff the three calls
Use JSON Diff to watch field names and enums drift—keep that as a regression baseline.
FAQ
Is Claude Fable 5.1 JSON output reliable?
On the output_config.format path, valid syntax and field conformance are API guarantees. Business meaning, numeric ranges, and string length are not—the decoder never sees those constraints.
Can I still use tool_choice: any?
No. Fable 5.1 / Mythos 5.1 return 400 for any and tool. Switch to auto + strict, or put the final shape on output_config.format. Name the tool in the prompt when it must run.
When to pick Fable vs OpenAI vs Gemini?
For long-horizon agents and hard extraction, Fable 5.1’s schema constraints are enough—but it is slow and expensive. Routine CRUD structured output should start on Sonnet 5 or Gemini. Multi-model routers should share the common schema subset.
Do I still validate after Structured Output?
Yes. API constraints do not replace try/except + validate. Empty bodies, truncated streams, and enum casing can still slip through.
Does the old output_format field still work?
REST accepts it for a transition window, but Python SDK 1.0+ messages.create() raises TypeError. New code should use output_config.format; the parse() helper may still take the convenience name output_format.
Итог
Claude Fable 5.1 JSON output in three lines:
Structured Output is GA; forced tools are gone; unsupported schema keywords 400.
Do not stop at one happy-path demo. Freeze a shared schema, score conformance, encode the breaking changes as regressions, and diff the three vendor payloads in JSONNote—closer to production than another “please return JSON” prompt.