---
title: "Structured Outputs on Bedrock: The Shape Is Guaranteed, the Content Is Not"
date: 2026-10-22
author: Jonathan Muszkat
canonical: https://me.jonymusky.com/blog/bedrock-structured-outputs-measure-first
---

# Structured Outputs on Bedrock: The Shape Is Guaranteed, the Content Is Not

On October 20, AWS announced [structured outputs for Claude models on Amazon Bedrock](https://aws.amazon.com/about-aws/whats-new/2026/10/claude-structured-outputs-bedrock/): you pass a JSON schema and the response is guaranteed to match it. The pitch is that you no longer need custom parsing, retries or post-processing.

We run a lot of structured calls through Bedrock, all with the same pattern: ask for JSON in the prompt, validate it with Zod, and repair or resample when it does not fit. So the announcement read like an invitation to delete a layer of code. Before deleting anything, we measured it.

Short version: it works, it is not a drop-in replacement, and for now we are keeping the old path as the default. This post covers what we found, without the specifics of our own workload.

## Before GA: "supported", until the schema got big

Before the announcement, native structured output on Bedrock with the newer Claude models was a mixed bag. Small schemas went through. A realistic extraction schema failed with an error like this:

`Schemas contains too many parameters with union types (76 ...) limit: 16`

Every optional or nullable field counts as a union type (`string | null`, which is what `.nullable()` produces in Zod). An extraction schema where most fields can be missing hits a cap of 16 very quickly. In practice, "not supported" meant "rejected once the schema is real". That is why we moved to prompt plus validation in the first place.

## The SDK has its own opinion

We call Bedrock through the Vercel AI SDK (`@ai-sdk/amazon-bedrock`). It has a `structuredOutputMode` option with three values: `outputFormat` (native), `jsonTool` (a forced tool call) and `auto`. Three details matter more than the option itself:

- **`auto` does not pick native for the newest Claude models.** As of version 5.0.121, the provider keeps a list of the Claude models it trusts with native structured output. Newer generations are excluded, and a comment in the source still says Bedrock rejects them. The GA happened, but the SDK has not caught up.

- **It cannot tell that an application inference profile is Claude.** If you call models through profile ARNs, which are opaque IDs, `auto` does not know it is talking to Anthropic. You have to set `modelFamily: 'anthropic'` or force the mode.

- **It strips constraints from your schema.** Before sending it, the SDK turns `maxLength`, `maximum` and `maxItems` into hints in the description. The model sees "keep this short" instead of a hard limit, so it overshoots. A schema with `.max()` constraints then fails your own validation on a response that Bedrock considered valid.

Forcing native mode looks like this:

```
const { output } = await generateText({
  model: bedrock(profileArn),
  output: Output.object({ schema }),
  providerOptions: {
    bedrock: { structuredOutputMode: 'outputFormat' },
  },
});
```

And because of the third point, you still need a repair step: something that trims overlong fields before your validator rejects the whole object. Native mode did not let us remove that code. It moved it to a different hook.

## How we measured

We took real inputs from several structured operations, from short classifications to long nested reports, and ran each one three ways:

- the current path (JSON in the prompt plus validation),

- the **same current path again**,

- native structured outputs.

The second run is the important one. LLM output varies from run to run even with nothing changed, so a difference between two models means nothing until you compare it with the difference between a model and itself. We call that the noise floor. Every comparison below is against it.

Same model and same reasoning effort in all three arms. The only thing that changed was how the schema reached the model.

## Finding 1: the shape is guaranteed, the content is not

This is the one that matters. On a nested schema, with a summary string plus an array of per-item evaluations, native mode often returned valid but hollow JSON: an empty summary, an empty array, every required key present. It happened in roughly two out of three runs. It also happened with and without reasoning effort, so it was not a thinking-budget problem.

Nothing failed. The schema said `summary: string`, and `""` is a string. Zod accepted it, and the call looked faster and cheaper. The token and latency savings were the missing content.

That is the trap with guaranteed schemas: they move the failure from "it threw" to "it looked fine". If you go native, give every field that must carry content a minimum (`.min(1)` on strings and on arrays that should never be empty), so that a hollow answer fails validation and gets retried instead of reaching a user.

## Finding 2: where it clearly helped

One schema went the other way. On the current path, a noticeable share of responses failed validation (around a quarter), mostly through structure problems the repair could not fix. With native mode, every response was valid, and the median latency was about half.

So it is not that native is worse. Shape guarantees are worth a lot on schemas where the model keeps getting the shape wrong. They are worth very little on schemas the model already gets right, and they can hurt where empty values pass as valid.

## Finding 3: everything else was a wash

On short classifications, comparisons and mid-sized reports, native and text paths differed by about as much as the text path differed from itself. One operation was a bit slower in native mode and had a couple of failures we never saw on the text path. Another looked slightly worse in agreement. The sample sizes were small enough that we would not call either a trend.

Two smaller costs: the native path added about a hundred input tokens per call, and we have not yet measured how moving the schema out of the prompt affects prompt caching.

## Side notes on the newer Claude models

None of this is specific to structured outputs, but it showed up in every run:

- **They think by default, inside your output budget.** Thinking tokens count toward `maxOutputTokens`. A budget tuned for an older model can be spent on thinking and come back cut mid-JSON, or empty. Set the reasoning effort explicitly and give the budget headroom.

- **They write more.** For the same task we saw several times the output tokens of the previous generation. Recheck every hard-coded limit.

- **They reject non-default sampling parameters** such as `temperature` when thinking is on. If your code sends `temperature: 0` for determinism, that request either fails or gets retried without it. Determinism has to come from somewhere else.

## What we are doing

For now, prompt plus validation stays the default. Native structured outputs become a per-schema opt-in. A schema qualifies when the text path has a real validation failure rate, the fields that must carry content have minimums, and a run against the noise floor shows that it helps.

The repair layer stays either way, because the SDK still strips the constraints it is supposed to enforce.

We will revisit this as the SDK learns about the newer models and as the native path matures. The feature is real and the GA is good news. It just does not mean what "no more post-processing" sounds like yet.

If you are about to delete your validation layer because of this announcement, measure first: the same model twice, then native, on your own inputs. And look at what the valid responses contain, not just whether they parse.
