Amazon Bedrock Structured Outputs for Claude: JSON Schema Responses With Converse (outputConfig.textFormat), InvokeModel and the Messages API, the "Schema Must Be a String" Trap, Supported Schema Features and Strict Tool Use
Amazon Bedrock structured outputs make Claude return JSON that always matches a schema you define, with no "please reply in JSON" prompting, parsing guesswork or retry loops. Support for all Claude models arrived on October 20, 2026. How you ask depends on which Bedrock API you call. With the Converse API, pass outputConfig={"textFormat": {"type": "json_schema", "structure": {"jsonSchema": {"schema": ..., "name": ...}}}}. With InvokeModel or the Anthropic SDK, put "output_config": {"format": {"type": "json_schema", "schema": {...}}} in the request. The trap that costs people their first hour is in Converse: the schema must be a JSON string, not an object. Pass a Python dict and boto3 rejects the request before it leaves your machine with Invalid type for parameter outputConfig.textFormat.structure.jsonSchema.schema ... valid types: <class 'str'>. Wrap it in json.dumps() and it works.
Jake's shop gets supplier invoices as PDFs and emails, and Ethan had built a small Lambda function that asked Claude on Bedrock to pull out the supplier, the date and the total. It worked nine times out of ten. The tenth time Claude added a friendly sentence before the JSON, or called the field "amount" instead of "total", and the function crashed. "It's like a new employee who's brilliant but won't use the form," Jake said. Structured outputs is the form.
This guide shows structured outputs for Claude on Amazon Bedrock in all three ways you can call it, with Python code, the schema rules that decide whether your request is accepted, strict tool use, and every validation error word for word so you can match it to your own. If you are new to calling Claude on Bedrock at all, Claude Sonnet 5.5 on Amazon Bedrock covers model IDs, inference profiles and the first request.
What structured outputs are, in plain English
Without structured outputs, getting JSON from a language model is a negotiation. You describe the format in the prompt, the model usually follows it, and your code wraps the parse in try, strips stray sentences and Markdown fences, and retries when a field is missing or named differently. At scale, that one failure in ten or one in a thousand becomes a queue of broken records.
Structured outputs remove the negotiation. You send a JSON Schema with the request: the fields, their types, which are required. The model's output is constrained while it is being generated, so the response is valid JSON that matches the schema: the right field names, the right types, nothing extra. There are two halves:
- JSON outputs: the model's text answer itself is the JSON object your schema describes. Use this for extraction, classification, summaries into fields, and anything where the answer is data.
- Strict tool use: when the model calls one of your tools, the arguments are guaranteed to match that tool's input schema. Use this for agents, where a malformed tool call means a failed action.
On Bedrock, both work with Claude models across the bedrock-runtime APIs (Converse and InvokeModel) and the bedrock-mantle Messages API endpoint, in the Regions where each Claude model is available.
Jake: "So instead of asking nicely, you hand it the form."
Ethan: "And it physically can't write outside the boxes."
One feature, three spellings: Converse, InvokeModel and the Messages API
| How you call Claude | Where the setting goes | Schema is |
|---|---|---|
| Converse / ConverseStream (boto3, AWS SDK for JavaScript v3, AWS CLI) | outputConfig.textFormat = {type: "json_schema", structure: {jsonSchema: {schema, name, description}}} | A JSON string |
| InvokeModel / InvokeModelWithResponseStream | "output_config": {"format": {"type": "json_schema", "schema": ...}} in the request body, next to messages | A JSON object |
| Anthropic SDK with the Bedrock Mantle client (Messages API) | output_config={"format": {...}} on messages.create(), or output_format= a Pydantic model on messages.parse() | An object, or a Pydantic class |
The names differ because the APIs differ. Converse is Bedrock's own model-neutral API, with camelCase fields and its own types, and its service model declares the schema as a string. InvokeModel and the Messages API pass Anthropic's own request format through, where output_config.format is the field. Mixing them up is the most common mistake; the errors section shows what each mix-up prints.
Structured outputs with the Converse API in Python (boto3)
This extracts an invoice into three fields. Note the json.dumps() around the schema:
import json
import boto3
client = boto3.client("bedrock-runtime", region_name="us-east-1")
invoice_schema = {
"type": "object",
"properties": {
"supplier": {"type": "string"},
"invoice_date": {"type": "string", "format": "date"},
"total": {"type": "number"},
"currency": {"type": "string", "enum": ["USD", "EUR", "GBP"]},
},
"required": ["supplier", "invoice_date", "total", "currency"],
"additionalProperties": False,
}
response = client.converse(
modelId="global.anthropic.claude-sonnet-5-5",
messages=[{"role": "user", "content": [{"text": "Extract the invoice fields:\n" + invoice_text}]}],
inferenceConfig={"maxTokens": 1024},
outputConfig={
"textFormat": {
"type": "json_schema",
"structure": {
"jsonSchema": {
"schema": json.dumps(invoice_schema), # a string, not a dict
"name": "invoice",
"description": "Fields extracted from one supplier invoice",
}
},
}
},
)
data = json.loads(response["output"]["message"]["content"][0]["text"])
print(data["supplier"], data["total"])
The answer arrives as ordinary text in the first content block, and that text is the JSON object. json.loads cannot fail on it unless the response was cut short, which the limits section explains how to detect. The same outputConfig works on converse_stream. Use the inference profile ID your account uses for the model; global.anthropic.claude-sonnet-5-5 is the global cross-Region profile.
In the AWS SDK for JavaScript v3 the shape is the same, camelCase included, and the schema is again a string:
import { BedrockRuntimeClient, ConverseCommand } from "@aws-sdk/client-bedrock-runtime";
const client = new BedrockRuntimeClient({ region: "us-east-1" });
const out = await client.send(new ConverseCommand({
modelId: "global.anthropic.claude-sonnet-5-5",
messages: [{ role: "user", content: [{ text: "Extract the invoice fields:\n" + invoiceText }] }],
inferenceConfig: { maxTokens: 1024 },
outputConfig: {
textFormat: {
type: "json_schema",
structure: { jsonSchema: { schema: JSON.stringify(invoiceSchema), name: "invoice" } },
},
},
}));
const data = JSON.parse(out.output.message.content[0].text);
For the basics of the Converse API itself, messages, system prompts, images and streaming, see the Bedrock Converse API in boto3.
Structured outputs with InvokeModel
InvokeModel takes the Anthropic Messages request as the JSON body, so the field is Anthropic's output_config.format and the schema is a normal object:
import json
import boto3
client = boto3.client("bedrock-runtime", region_name="us-east-1")
body = {
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Extract the invoice fields:\n" + invoice_text}],
"output_config": {
"format": {
"type": "json_schema",
"schema": invoice_schema, # an object here
}
},
}
response = client.invoke_model(modelId="global.anthropic.claude-sonnet-5-5", body=json.dumps(body))
result = json.loads(response["body"].read())
text = next(block["text"] for block in result["content"] if block["type"] == "text")
data = json.loads(text)
Because the body is opaque to boto3, a mistake here is not caught on your machine: the model endpoint returns a validation error instead. If you also set effort, it goes in the same object: "output_config": {"effort": "low", "format": {...}}.
Structured outputs with the Anthropic SDK on Bedrock (Messages API)
The Anthropic Python SDK has a Bedrock client for the bedrock-mantle Messages endpoint, and its messages.parse() helper turns a Pydantic model into the schema and the reply back into a validated object:
pip install -U anthropic pydantic
from pydantic import BaseModel
from anthropic import AnthropicBedrockMantle
class Invoice(BaseModel):
supplier: str
invoice_date: str
total: float
currency: str
client = AnthropicBedrockMantle(aws_region="us-east-1") # uses your normal AWS credentials
response = client.messages.parse(
model="anthropic.claude-sonnet-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Extract the invoice fields:\n" + invoice_text}],
output_format=Invoice,
)
invoice = response.parsed_output # an Invoice instance, already validated
print(invoice.supplier, invoice.total)
Prefer this route for new Python or TypeScript code: the SDK removes schema features the API does not support (such as numeric minimums) before sending, and checks them on the reply instead, so your Pydantic model can stay as strict as you like. Bedrock model IDs on this client take the anthropic. prefix. The same output_config={"format": {"type": "json_schema", "schema": {...}}} also works on plain messages.create().
Which JSON Schema features are supported
| Supported | Not supported |
|---|---|
| Types: object, array, string, integer, number, boolean, null | Recursive schemas |
enum, const, anyOf, allOf, $ref / $defs | Numeric limits: minimum, maximum, multipleOf |
String formats: date-time, time, date, duration, email, hostname, uri, ipv4, ipv6, uuid | String length limits: minLength, maxLength |
additionalProperties: false (required on every object) | Complex array constraints; additionalProperties set to anything except false |
Two rules catch most rejected schemas. Every object, including nested ones and objects inside arrays, needs "additionalProperties": false. And list the fields in required; for a field that may be missing in the source document, make its type nullable ({"type": ["string", "null"]} or an anyOf with null) rather than leaving it out of required, so your code always gets the key. Business rules such as "total must be positive" belong in your code after parsing, or in a Pydantic validator when you use the SDK.
For enums, list exactly the values your code handles. In Jake's invoice schema, currency can only ever be one of three strings, which turned a free-text field into a reliable switch statement.
Strict tool use, and the forced tool choice that no longer works
For agents, set strict on each tool so its arguments always validate. In Converse the field sits in the tool specification:
tool_config = {
"tools": [{
"toolSpec": {
"name": "record_invoice",
"description": "Save one supplier invoice to the books",
"strict": True,
"inputSchema": {"json": invoice_schema},
}
}]
}
response = client.converse(modelId="global.anthropic.claude-sonnet-5-5",
messages=messages, toolConfig=tool_config,
inferenceConfig={"maxTokens": 1024})
In InvokeModel and the Messages API, "strict": true is a top-level field on the tool definition, next to name, description and input_schema, and the schema needs additionalProperties: false and required as above.
This matters more on the newest models. Claude Sonnet 5.5 and Claude Opus 5.5 reject forced tool use: a request with tool_choice of any or a named tool fails with tool_choice: type "tool" and "any" are not supported for this model. Code that used a forced tool call only to get JSON back should switch to JSON outputs; code that needs the model to call a tool should use auto, say which tool to use in the prompt, mark the tool strict, and check that a call was made.
The exact errors, and what each one means
Converse requests are checked by the SDK before they are sent, so these appear instantly, with no AWS call made:
ParamValidationError: Parameter validation failed: Invalid type for parameter outputConfig.textFormat.structure.jsonSchema.schema, value: {...}, type: <class 'dict'>, valid types: <class 'str'>
You passed the schema as a dict. Use json.dumps(schema) (or JSON.stringify in JavaScript).
Unknown parameter in outputConfig: "format", must be one of: textFormat, effort
You used the InvokeModel / Messages API spelling in a Converse call. In Converse it is textFormat with a structure.jsonSchema inside.
Missing required parameter in outputConfig.textFormat: "structure"
The schema is missing its wrapper: textFormat needs both type and structure: {jsonSchema: {schema: ...}}.
Unknown parameter in input: "outputConfig"
Your boto3 or botocore is older than the feature. Upgrade with pip install -U boto3; on Lambda, package a current boto3 with the function or in a layer, because the runtime's built-in copy can lag behind.
tool_choice: type "tool" and "any" are not supported for this model.
Forced tool use on Sonnet 5.5 or Opus 5.5. Use JSON outputs, or auto with a strict tool, as in the previous section.
Errors in the schema itself (an unsupported keyword, a missing additionalProperties: false) come back from Bedrock as a ValidationException naming the problem, on every API. Read the message: it points at the part of the schema it rejected.
Ethan's first attempt hit the dict error, his second the format one, because he had copied an InvokeModel example into a Converse call. "Two errors in two minutes," he told Jake, "and neither of them cost a cent, because the SDK caught both before anything was sent."
Moving an existing "reply in JSON" app to structured outputs
- Write the schema from what your code reads, not from the prompt: every key your code accesses becomes a property, with its type, in
required, andadditionalProperties: falseon every object. - Add the setting for your API (Converse
outputConfig.textFormatwith the schema string, oroutput_config.format), and upgrade boto3 first if it is older than October 2026. - Delete the format instructions from the prompt ("reply only in JSON", "use these keys"). Keep the task instructions, and move field explanations into each property's
description. - Replace the clean-up code (stripping Markdown fences, regex extraction, retry-on-parse-error) with a stop-reason check followed by a plain
json.loads. - Run your old test inputs through both versions and compare field by field before switching traffic.
And when you add a new field later:
- Add it to the schema, nullable if older documents will not have it.
- Deploy the code that reads it before or with the schema change, so nothing breaks on the first reply that includes it.
- Expect one slower request while the new schema compiles.
Limits and behavior to plan for
- First request with a new schema is slower: the schema is compiled once, then cached for 24 hours, so keep schemas stable rather than generating a new one per request.
- Truncation: if the response stops because it reached the token limit (
stopReasonmax_tokensin Converse,stop_reasonin the Messages API), the JSON can be incomplete. Check the stop reason before parsing and raise the limit. - Refusals and other stops: if the model declines a request for safety reasons, the output may not match the schema. InvokeModel and the Messages API report
stop_reasonrefusal; Converse reports its own values, so in Converse parse only whenstopReasonisend_turnand treatcontent_filtered,malformed_model_outputormax_tokensas a failed extraction. - Not combinable with citations (a 400) or assistant prefill, which current Claude models reject anyway.
- Works with streaming, batch inference, token counting and extended thinking.
- Cost: no separate fee is listed for the feature; you pay for input and output tokens as usual, and replies that contain only your schema's fields are often shorter than free-form ones. See Bedrock pricing for the token rates.
Troubleshooting by symptom
ParamValidationError about jsonSchema.schema
The schema is a dict; Converse wants a string. Wrap it in json.dumps().
Unknown parameter outputConfig
boto3 is too old. Upgrade it, and on Lambda ship your own boto3 rather than relying on the runtime's copy.
ValidationException naming a schema keyword
The schema uses something unsupported, such as minimum or maxLength, or an object lacks additionalProperties false. Remove or move the rule into your code.
json.loads fails on the reply
Check the stop reason: max_tokens means the reply was cut off; refusal (Messages API) or content_filtered (Converse) means the model declined. Raise the token limit or handle the refusal.
The JSON is valid but a field is empty or null
The source text did not contain it, and the schema allowed null. That is correct behavior; decide in code what a missing value means.
The first request takes several seconds longer
Schema compilation on first use. Later requests with the same schema in the next 24 hours are faster.
My forced tool call now returns a 400
Sonnet 5.5 and Opus 5.5 reject tool_choice any or tool. Switch to JSON outputs, or auto plus a strict tool.
It works locally but not in Lambda
Lambda's built-in boto3 may predate the feature. Package a current boto3 with the function.
Frequently asked questions
Does Amazon Bedrock support structured output?
Yes. Since October 20, 2026, all Claude models on Bedrock support structured outputs on bedrock-runtime (Converse and InvokeModel) and on the bedrock-mantle Messages API endpoint.
Does Bedrock support structured output for Claude?
Yes, for every Anthropic Claude model, wherever that model is available on Bedrock.
How do I use structured output with the Bedrock Converse API?
Pass outputConfig with textFormat type json_schema and structure.jsonSchema containing your schema as a JSON string and a name.
Why does Converse say the schema must be a string?
The Converse API declares jsonSchema.schema as a string. Serialize your schema with json.dumps() in Python or JSON.stringify() in JavaScript.
How do I get JSON output from Claude with InvokeModel?
Add "output_config": {"format": {"type": "json_schema", "schema": {...}}} to the request body next to messages, with the schema as an object.
What is the difference between output_config and outputConfig?
output_config is Anthropic's Messages API field used by InvokeModel and the Anthropic SDK; outputConfig is the Converse API field, with textFormat inside.
Is output_format deprecated?
As a raw API field, yes: use output_config.format. The Anthropic SDK's messages.parse() still accepts output_format as a convenience for passing a Pydantic model.
What JSON Schema features does structured output support?
Basic types, enum, const, anyOf, allOf, $ref and common string formats, with additionalProperties false on every object. Recursive schemas and min/max or length limits are not supported.
Do I still need to ask for JSON in the prompt?
No. Describe the task; the schema enforces the format. Field descriptions in the schema help the model fill each field correctly.
What is strict tool use?
strict: true on a tool definition guarantees that the model's arguments for that tool validate against its input schema.
Why does tool_choice any fail on Claude Sonnet 5.5?
Sonnet 5.5 and Opus 5.5 do not support forced tool use. Use JSON outputs, or tool_choice auto with a strict tool and a clear instruction.
Does structured output work with streaming on Bedrock?
Yes, with ConverseStream and InvokeModelWithResponseStream. Parse the JSON once the stream completes.
Does structured output cost extra on Bedrock?
No separate fee is listed; you pay normal input and output token prices for the model.
Can I use structured outputs with citations?
No. Combining structured outputs with citations returns a 400 error.
Why is the first structured output request slow?
A new schema is compiled on first use and cached for 24 hours, so the first request takes longer than later ones.
Can the JSON still be invalid?
Only if the reply was cut off at the token limit or the model refused. Check the stop reason before parsing.
For readers preparing for an AWS certification
We have been covering AWS certifications alongside our fixes, so here is a short extra for readers studying for one. If you only came for the fix, you already have it; feel free to skip this part.
Exams that test this topic: AI Practitioner (AIF-C01) • Developer – Associate (DVA-C03 from December 1, 2026) • Machine Learning Engineer – Associate (MLA-C02).
Here are the exam-style questions this topic tends to raise, with the reasoning you need to answer them:
1. A Lambda function extracts invoice fields with Claude on Amazon Bedrock and sometimes fails to parse the reply as JSON. Which Bedrock feature guarantees schema-valid JSON?
Structured outputs: outputConfig.textFormat with a JSON schema in Converse, or output_config.format in InvokeModel.
2. A developer passes a Python dict as the JSON schema in a Converse request and boto3 raises ParamValidationError. What is the fix?
Serialize the schema with json.dumps(); Converse declares jsonSchema.schema as a string.
3. Which setting guarantees that an agent's tool arguments match the tool's input schema?
strict: true on the tool definition (toolSpec.strict in Converse).
4. Code that forces a tool call with tool_choice any fails on Claude Sonnet 5.5 on Bedrock. What should replace it when the goal is to get JSON back?
Structured outputs (JSON outputs), or tool_choice auto with a strict tool and a prompt instruction.
5. A structured output reply cannot be parsed. Which response field should be checked first?
The stop reason: max_tokens means truncation and a refusal or content filter means the model declined.
6. Which JSON Schema rule must every object in a structured output schema follow?
additionalProperties must be false, with the fields listed in required.
Exam codes and dates change often. Our AWS certification changes page keeps track of what is current.
The invoice function has not crashed since. Ethan deleted forty lines of cleanup code that stripped stray sentences and Markdown fences from Claude's replies, and the function got shorter, faster and cheaper. "The best code I wrote this month," he said, "was the code I deleted."
📌 If you keep one line from this page
On Bedrock, ask Converse for JSON with outputConfig.textFormat and a schema string, or InvokeModel and the Anthropic SDK with output_config.format and a schema object; set additionalProperties false on every object, and use strict tools instead of forced tool choice.
Structured outputs for all Claude models on Bedrock since October 20, 2026 · schema cached 24 hours · not combinable with citations.
Revision note. Written October 23, 2026, with request validation from boto3 and botocore 1.43.118 and the Anthropic Python SDK 2.1.0, and Bedrock's structured output support as it stood on that date. "Say what you mean and mean what you say," the saying goes; a schema lets your code do both.