Table of Contents
Bedrock structured outputs for Claude: stop parsing broken JSON
Ask Claude for JSON and you will eventually get a response that almost parses. A trailing comma, a markdown code fence Claude added out of habit, a field that is a string in one run and a number in the next. We have shipped retry wrappers around exactly this problem more than once. As of October 20, 2026, Amazon Bedrock closes that gap for Claude: every supported model now returns JSON that is validated against a schema you define, on both the bedrock-runtime and bedrock-mantle endpoints, per AWS. No retry logic. No post-processing pass to coerce types.
This guide covers what the feature actually guarantees, which models and endpoints carry it, the request shape for both mechanisms Bedrock exposes, where it still breaks, and when to reach for Bedrock's version instead of calling Claude directly through Anthropic's own API.
What changed, mechanically
Bedrock gives Claude two related but distinct guarantees, documented in the Bedrock structured outputs reference:
- JSON Schema output format. You pass a schema; Claude's entire text response conforms to it. Use this for extraction, report generation, and API responses.
- Strict tool use. Add
"strict": trueto a tool definition and the tool's arguments are validated against itsinput_schemaat generation time, not after the fact. Use this for agent tool calls and multi-step automation.
They stack. A single request can define a response schema and a strict tool in the same call.
Under the hood, Bedrock validates your schema against a supported subset of JSON Schema Draft 2020-12 the first time it sees it. If the schema uses an unsupported feature, you get a 400 error immediately, not a failed generation three steps into a pipeline. New schemas compile into a constrained grammar, which the Bedrock docs say can take "up to a few minutes" on first use; compiled grammars are then cached for 24 hours, so repeat requests with the same schema run at normal inference latency. That first-compile tax is the one planning detail worth building into a deploy checklist: warm the schema before traffic hits it, not during a launch.
Source: aws.amazon.com, captured October 21, 2026.
Which models and endpoints actually support it
Support spans the Claude model lineup, but it is not uniform. Per the Bedrock docs, structured outputs is available "across the Claude Sonnet, Opus, Haiku, Fable, and Mythos model families," and support varies by model and endpoint: some models land on bedrock-runtime only, some on bedrock-mantle only, many on both. On bedrock-runtime you can reach it through the Converse API, the InvokeModel API, or the Anthropic Messages API. On bedrock-mantle, which AWS designed for agentic workloads with server-side tools and stateful conversations, you use the Anthropic Messages API directly. Check the specific model's card before you build against it; the docs also flag that some geographic cross-Region inference profiles drop support for specific models (Claude Haiku 4.5 through the India inference profile is the example AWS calls out), so a model that works in us-east-1 is not guaranteed to work identically through every routing profile.
How to turn it on
On bedrock-runtime, if you are calling the InvokeModel API with the Anthropic-native request body, the schema goes directly into output_config.format as a JSON object:
import boto3
import json
bedrock_runtime = boto3.client("bedrock-runtime", region_name="us-east-1")
schema = {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Customer name"},
"email": {"type": "string", "description": "Customer email address"},
"plan_interest": {"type": "string", "description": "Product plan of interest"},
"demo_requested": {"type": "boolean", "description": "Whether a demo was requested"},
},
"required": ["name", "email", "plan_interest", "demo_requested"],
"additionalProperties": False,
}
response = bedrock_runtime.invoke_model(
modelId="global.anthropic.claude-sonnet-5-5",
body=json.dumps({
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 512,
"messages": [{"role": "user", "content": (
"Paulo Santos ([email protected]) is interested in our Enterprise "
"plan and wants to schedule a demo for next Tuesday at 2pm."
)}],
"output_config": {"format": {"type": "json_schema", "schema": schema}},
}),
)
body = json.loads(response["body"].read())
if body["stop_reason"] == "end_turn":
result_text = next(b["text"] for b in body["content"] if b["type"] == "text")
print(json.dumps(json.loads(result_text), indent=2))
(Converse API users get the same mechanism through outputConfig.textFormat instead; the shape is in the Bedrock docs.)
On bedrock-mantle, where agentic tool calling lives, strict tool use is the mechanism:
from anthropic import AnthropicBedrockMantle
client = AnthropicBedrockMantle(aws_region="us-east-1")
message = client.messages.create(
model="anthropic.claude-sonnet-5",
max_tokens=512,
messages=[{"role": "user", "content": "What's the weather like in San Francisco?"}],
tools=[{
"name": "get_weather",
"description": "Get the current weather for a specified location",
"strict": True,
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "The city and state, e.g., San Francisco, CA"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["location", "unit"],
"additionalProperties": False,
},
}],
)
for block in message.content:
if block.type == "tool_use":
print(f"Tool: {block.name} Input: {block.input}")
With strict: true set, location is always a string and unit is always celsius or fahrenheit; the model cannot add a field the schema does not define. Both examples follow the request shapes documented in the Bedrock structured outputs reference.
Three paths to the same guarantee
The hard part isn't the feature, it's picking which surface to build against. AWS's own Supported APIs or features table lists Converse, ConverseStream, InvokeModel, cross-Region inference, batch inference, and the Messages API on bedrock-mantle, all as supported paths, but they sit on top of different SDKs with different ergonomics.
| Path | API surface | Schema mechanism | Best for |
|---|---|---|---|
bedrock-runtime | Converse, InvokeModel, ConverseStream | output_config.format / outputConfig.textFormat, raw JSON Schema | Teams already on boto3, single-turn extraction jobs, batch inference pipelines |
bedrock-mantle | Anthropic Messages API | strict: true tool definitions | Agentic workflows that need server-side tools and want to stay inside an AWS account for IAM and billing |
| Direct Anthropic API | Messages API (native) | client.messages.parse() with a Pydantic or type-based output_format, or a raw schema in output_config.format | A typed object in code with SDK-managed schema translation; no AWS account or IAM setup required |
The direct Anthropic API path is worth calling out because it is not the same developer experience. Anthropic's SDKs ship a helper, client.messages.parse(), that takes a Pydantic model (or an equivalent typed object in other languages) and does the schema translation for you: it strips unsupported constraints like minimum or maxLength, folds them into the field's description instead, adds additionalProperties: false automatically, and then validates the response against your original schema, constraints included, in your own code, per Anthropic's docs. On Bedrock's bedrock-runtime path, you build the simplified schema yourself, by hand, in output_config.format. That is not a limitation so much as a different point on the build-it-yourself-versus-managed spectrum, and it is the detail that should drive your pick more than the headline feature parity does.
Where it still breaks
Structured outputs on Bedrock is a constrained subset of JSON Schema, not the full spec, and the gaps are specific enough to design around rather than discover in production:
- No recursive schemas and no external
$refreferences (internal$ref/$def/definitionsare fine). - No numerical constraints:
minimum,maximum, andmultipleOfare unsupported. - No string length constraints:
minLengthandmaxLengthare unsupported. additionalPropertieshas to befalse. Anything else is rejected.- It is incompatible with citations. Turn on citations for an Anthropic model while structured outputs is active and Bedrock returns a 400 error, per the Bedrock docs.
Source: docs.aws.amazon.com, captured October 21, 2026.
Practically, that ruling-out list means the usual validation work does not disappear, it moves. A field like confidence_score still needs a minimum: 0, maximum: 1 check somewhere; Bedrock just will not enforce it for you, so you add a thin validation step after you get the (type-correct, field-complete) JSON back. The schema still does the heavy lifting: it rules out missing fields, a string where you expected a boolean, and a stray markdown fence wrapped around the object.
Where this actually earns its keep
AWS points at financial-document extraction, parsing clinical notes into EHR records, contract analysis for key terms and obligations, product catalog enrichment, and support-ticket routing, where intent, sentiment, and category always need to match a fixed downstream schema. The pattern across all five: you are feeding a system that expects a strict data model (a CRM field, a database column, a routing table) and the old failure mode was Claude being "close enough" instead of exactly right.
Ticket classification is the cleanest version of it, because the schema doubles as the business logic:
schema = {
"type": "object",
"properties": {
"category": {"type": "string", "enum": ["billing", "technical", "account", "sales", "other"]},
"priority": {"type": "string", "enum": ["low", "medium", "high", "urgent"]},
"sentiment": {"type": "string", "enum": ["positive", "neutral", "negative"]},
"escalate": {"type": "boolean"},
"reason": {"type": "string", "description": "One-sentence justification"},
},
"required": ["category", "priority", "sentiment", "escalate", "reason"],
"additionalProperties": False,
}
Run a lightweight model like Claude Haiku 5.5 against that schema and escalate is always a real boolean you can branch on in code, never a string like "true" that silently passes an if check it shouldn't. The AWS example of this pattern uses Haiku specifically because classification and routing are high-volume, repetitive workloads where a smaller model with a guaranteed-valid output beats a larger model you still have to double-check.
The verdict
If your pipeline already lives inside AWS (IAM roles, CloudTrail, an existing bedrock-runtime or bedrock-mantle integration), use Bedrock's structured outputs and stop writing retry wrappers around Claude's JSON. Pick bedrock-runtime with output_config.format for single-turn extraction jobs bolted onto an existing boto3 pipeline; pick bedrock-mantle with strict tool use for agent workflows that need server-side tool calls and want to stay inside your AWS billing and audit perimeter.
If you are starting from zero, outside AWS, and want the schema translation and response validation handled for you in a couple of lines, call Claude through the direct Anthropic API and use client.messages.parse() with a Pydantic model. You get the same schema-compliance guarantee, per Anthropic's docs, with less schema plumbing to maintain. Either way, the retry-loop-around-broken-JSON pattern that has been standard practice since function calling first shipped is now optional, not load-bearing.