Bedrock structured outputs stop at Claude 4.6 — a prompt rule and error feedback for Sonnet 5
Amazon Bedrock rejects strict tool use on Claude Sonnet 5. A prompt rule and a tool_result error retry kept translation JSON valid without it.

On this page
Introduction
A document translation pipeline I work on started failing batches with ValueError: 応答要素の形式が不正: ['id', 'text']: a response element with the wrong keys. Claude Sonnet 5 was forced to answer through a tool whose schema asks for {id, translated_text}, and on some calls it named every key text instead, copying the input.
Strict tool use exists to make that impossible, and Amazon Bedrock rejects it for Sonnet 5 and every Claude model after Opus 4.6. A one-line prompt rule kept the JSON valid, backed by sending the validation error back to the model as a tool result. On Sonnet 5, both worked.
Where structured outputs stops on Bedrock
Structured outputs is the feature that compiles a JSON Schema into a grammar and lets the model sample only tokens the grammar allows, so the output cannot break the schema. Bedrock exposes it two ways, usable separately or in the same request:
strict: truesits on a tool definition and holds the call’s arguments to that tool’sinput_schemaoutput_config.formatcarries a JSON Schema of its own for the model’s plain JSON answer, with no tool involved
Both give the same guarantee to a different target, and both were tried on Sonnet 5.
Sending the same minimal request to every Claude inference profile in ap-northeast-1 split the models at the 4.6 generation:
body = { "anthropic_version": "bedrock-2023-05-31", "max_tokens": 200, "messages": [{"role": "user", "content": "a に hello を入れて"}], "tools": [{"name": "t", "description": "x", "input_schema": schema, "strict": True}], "tool_choice": {"type": "tool", "name": "t"},}client.invoke_model(modelId=f"global.anthropic.claude-{m}", body=json.dumps(body))| Model | strict: true on Bedrock |
|---|---|
| Haiku 4.5, Sonnet 4.5, Opus 4.5 | Accepted |
| Sonnet 4.6, Opus 4.6 | Accepted |
| Opus 4.7, Opus 4.8, Opus 5, Opus 5.5 | ValidationException |
| Sonnet 5, Sonnet 5.5 | ValidationException |
Every rejection carried the same message, and output_config.format failed the same way on Sonnet 5 while Haiku 4.5 answered with schema-shaped JSON:
ValidationException: tools.0.custom.strict: Extra inputs are not permittedValidationException: output_config.format: Extra inputs are not permittedThe Sonnet 5 model card agrees: its Capabilities and Features table lists Structured outputs under Not Supported, on both the bedrock-runtime and bedrock-mantle endpoints. Fable 5 and 5.1 could not be tested at all; both refused the request with data retention mode 'default' is not available for this model.
Is the limit Bedrock’s or Anthropic’s?
Anthropic’s own API supports structured outputs on Sonnet 5. Its structured-outputs page lists claude-sonnet-5, claude-opus-5-5 and claude-sonnet-5-5 among the supported models, and notes that on Amazon Bedrock only a smaller set (Opus 4.6, Sonnet 4.6, Sonnet 4.5, Opus 4.5 and Haiku 4.5) has it.
Bedrock compiles that grammar itself, from the JSON Schema in each request. Its structured-outputs page states under “Request workflow” that “Amazon Bedrock compiles the grammar, which may take up to a few minutes”.
The grammar pipeline is Bedrock’s own infrastructure, so support is decided per deployment rather than per model. The same page warns that structured outputs “aren’t supported for Claude Haiku 4.5 through the India (in.anthropic) inference profile”, a model that has it everywhere else.
Why each new Claude generation arrives on Bedrock without it is suspected, not documented. The likeliest reading of those two quotes is that every model has to be enabled in Bedrock’s own grammar engine, and that has not happened for anything newer than the 4.6 generation. AWS gives no reason and no date, though I expect support to roll out over time.
The Anthropic SDK on Bedrock
anthropic[bedrock] ships an AnthropicBedrock client that speaks the Messages API, which reads like a way around Converse, Bedrock’s own model-agnostic chat API. It signs the request and sends it to the same bedrock-runtime InvokeModel endpoint. The probe above already used that body shape through invoke_model, and it is the request Bedrock refused.
Why forced tool use still returned the wrong key
tool_choice: {"type": "tool"} guarantees that the model calls the tool. It does not look at what goes inside the call. Anthropic’s structured-outputs page is explicit that without it, “Claude can generate malformed JSON responses or invalid tool inputs”, including “Missing required fields”.
The pipeline sends each batch as a JSON array of {"id", "text"} objects in the user message, 63 of them for one Word document, and asks for {"id", "translated_text"} back.
When the model wrote text for the first element, it wrote text for all 63. The values were proper English translations, so the model had done the work and mislabelled it.
The rate depended on the prompt, and that one Word document reproduced it on demand:
| Prompt | Calls with every key named text |
|---|---|
| Without the soft length cap (translation about 2× the source) | 2 of 30 |
| With that cap, as in production | 8 of 40 |
One call in five broke at production settings, and a plain retry of the same prompt broke at the same rate.
A prompt rule that names the forbidden key
The first fix is one line appended to the end of the system prompt, after every other instruction. It says to return results through submit_translations, to use the keys id and translated_text, and never to use the input key text.
- 結果は `submit_translations` ツールで返すこと。`translations` の各要素のキーは `id` と `translated_text` にする。入力のキー `text` を出力に使ってはいけない (訳文は `translated_text` に入れる)- Return the results with the `submit_translations` tool. Each element of `translations` uses the keys `id` and `translated_text`. Do not use the input key `text` in the output (put the translation in `translated_text`)A version that stated only the correct keys, without mentioning text, still broke:
| Rule | Calls with every key named text |
|---|---|
| None | 8 of 40 |
| Correct keys only | 4 of 40 |
Correct keys, and text named as forbidden | 0 of 40 |
The rule costs 82 input tokens per call, on a 4,885-token prompt. Zero in 40 does not mean the rate is zero: 40 samples only bound the true rate below roughly 7.5%, so a second line of defence is needed for the calls that slip through.
Feeding the error back as a tool_result
Instead of sending the batch again, send the broken answer back with the error. That is how I had handled invalid SQL in a text-to-SQL project: run the query, return the database error, and let the model try again with it in view.
Anthropic documents the same move for tool calls: “continue the conversation forward with a tool_result that indicates the error, and Claude will try to use the tool again”.
On Bedrock’s Converse API that means appending the broken assistant turn unchanged and answering its toolUse with an error:
bad_use = next(b["toolUse"] for b in bad_content if "toolUse" in b)messages = [ {"role": "user", "content": [{"text": user}]}, {"role": "assistant", "content": bad_content}, # the response, unmodified {"role": "user", "content": [{"toolResult": { "toolUseId": bad_use["toolUseId"], "content": [{"text": feedback}], "status": "error", }}]},]client.converse(modelId=model_id, system=[{"text": system}], messages=messages, inferenceConfig=inference_config, toolConfig=tool_config)The feedback string says what was wrong and what to do: the received keys were ['id', 'text'], each element needs id and translated_text, and every element should be submitted again. Keep the assistant content byte-for-byte, because with adaptive thinking on it can carry reasoningContent blocks that have to go back with it.
Every real broken response from the measurements above, all eight, was fixed on the first retry. A retried call costs about 72% more input tokens than the original and the same 24 seconds.
Retrying underneath the response cache
The pipeline caches each model response in DynamoDB, keyed by a hash of the complete prompt, so a redelivered job replays the batches that already succeeded instead of paying for them again. Both fixes above have to sit underneath that cache, or the cache undoes them.
A cache that saves a response before anything checks its shape stores the broken batch, and every redelivery replays it. The second and third SQS deliveries each logged a cache hit for the same key and failed with the same ValueError. Validating before saving, and again on read, closes that.
The second trap is a retry that changes the prompt: feeding the error back appends the broken response and the error message to the request. Sonnet 5 also accepts no temperature other than the default 1.0, so every repeated call returns a different translation.
Each batch’s prompt also carries the previous batch’s translated tail as context. A batch fixed by a different prompt is saved under a key the next delivery never builds, and the miss propagates.
In a simulated five-batch document, with batch 1 broken once and the first delivery dying on batch 3, the second delivery called the model for batches 1 to 4 instead of 3 and 4.
Moving the retry inside the Bedrock adapter fixes both traps at once. The cache wrapper calls the adapter once with the original prompt; the adapter validates, feeds the error back if it has to, and returns either a valid response or the broken one. The cache stores only a valid response, under the original key.
Summary
| Problem | What handles it |
|---|---|
Bedrock rejects strict: true and output_config.format on Claude Sonnet 5 and every model after Opus 4.6 | Nothing on Bedrock’s side yet; validate in your own code |
Forced tool use renames translated_text to text in 8 of 40 calls | A system-prompt rule that names text as forbidden: 0 of 40 |
| A call that still breaks | Return it as a toolResult error with the keys it should have used: 8 of 8 fixed |
| A response cache keyed on the prompt | Validate before saving, and retry inside the model adapter so the key stays the original |
Structured outputs on Bedrock is a feature of the deployment, and the model card is where to check it before choosing a model. When it is missing, a validator that feeds its error back to the model buys most of the guarantee back for one extra call on the batches that need it.
References
- Amazon Bedrock: Get validated JSON results from models (structured outputs)
- Amazon Bedrock model card: Claude Sonnet 5
- Claude API: Structured outputs, with the supported model list and Bedrock availability
- Claude API: Strict tool use
- Claude API: Define tools, including forced tool use restrictions
- Claude API: Handle tool calls, including errors with is_error



