DEV Community

Cover image for Structured outputs for every Claude model on Amazon Bedrock: schema-valid JSON, plus an EKS alert-triage example
Matias Martinez
Matias Martinez

Posted on Originally published at blog.cloudacademy.ar

Structured outputs for every Claude model on Amazon Bedrock: schema-valid JSON, plus an EKS alert-triage example

Originally published in Spanish on CloudAcademy.ar.

If you've put an LLM in the middle of an automated pipeline, you know the routine: ask for JSON in the prompt, add an example, beg for no extra text, and still get the occasional trailing comma, renamed field or "Sure, here's the JSON:" that breaks json.loads. So you add validation, retries and a forgiving parser, and the glue code ends up bigger than the business logic.

On October 20, 2026, AWS announced structured outputs for all Anthropic Claude models on Amazon Bedrock, on both the bedrock-runtime and bedrock-mantle endpoints. You define a JSON Schema and the response follows it, because the constraint is applied during token generation rather than checked afterwards.

TL;DR

  • Two mechanisms: a JSON Schema output format (shapes the model's response) and strict tool use ("strict": true, shapes tool-call arguments). You can use both in the same request.
  • It's constrained decoding. The first request with a new schema compiles a grammar (up to ~90 seconds), which is cached for 24 hours per account.
  • Every object needs additionalProperties: false. Max 12 optional properties and 16 union-typed/anyOf properties. No minimum/maximum, no minLength/maxLength, no recursive schemas.
  • Incompatible with citations on Anthropic models (400 error).
  • No separate charge announced: you pay model tokens, and output usually gets shorter.
  • Still check stop_reason: hitting max_tokens truncates the JSON.

Why this matters now

Structured outputs on Bedrock isn't new. It went GA in February 2026 across several providers. The gap was coverage: the Claude 5 family wasn't supported for months. You could see it in the ecosystem. A July issue in Vercel's AI SDK reported Bedrock rejecting strict for claude-sonnet-5 with a 400, so the SDK limited native structured outputs to Claude 4.x. The day of the announcement, a new issue asked to enable it for Claude 5. Lesson: Bedrock supporting it doesn't mean your SDK sends it.

This launch closes that gap: every Claude model, both endpoints, plus batch inference, cross-Region inference and typed streaming events.

How it works

Structured outputs pipeline

  1. Bedrock validates the schema against a JSON Schema Draft 2020-12 subset (unsupported feature: immediate 400).
  2. A new schema compiles into a grammar, which can take up to ~90 seconds. AWS recommends a client read timeout of at least 120 seconds.
  3. The grammar is cached for 24 hours per account. Changing only name/description keeps the cache; structural changes don't.
  4. The model can only emit tokens that keep the output valid.

Output format vs strict tool use

Where the schema goes: output_config.format for InvokeModel and the Messages API (a JSON object), and outputConfig.textFormat for Converse (a JSON string inside structure.jsonSchema.schema).

Hands-on: alert triage on EKS with Claude Haiku 5.5

The example: Alertmanager sends a webhook to a pod on EKS, the pod asks Claude Haiku 5.5 to classify the alert group against a schema, and the JSON decides who gets paged.

Triage flow

Haiku 5.5 is what AWS suggests for fast, repetitive work like classification. I'm using the global inference profile, global.anthropic.claude-haiku-5-5, which is also the only way to reach Haiku 5.5 from sa-east-1.

Permissions via EKS Pod Identity (trust pods.eks.amazonaws.com, allow bedrock:InvokeModel on the inference profile and foundation model ARNs), then:

aws eks create-pod-identity-association --cluster-name production \
  --namespace observability --service-account alert-triage \
  --role-arn arn:aws:iam::123456789012:role/alert-triage-bedrock
Enter fullscreen mode Exit fullscreen mode

The schema keeps everything in required to stay under the 12-optional limit, and uses ["string", "null"] where a value may be absent:

TRIAGE_SCHEMA = {
    "type": "object",
    "properties": {
        "severity": {"type": "string", "enum": ["info", "warning", "critical"]},
        "owner_team": {"type": "string", "enum": ["platform", "payments", "data", "frontend", "unknown"]},
        "page_oncall": {"type": "boolean"},
        "probable_cause": {"type": "string"},
        "suggested_action": {"type": "string"},
        "runbook_url": {"type": ["string", "null"], "format": "uri"},
        "related_alerts": {"type": "array", "items": {"type": "string"}}
    },
    "required": ["severity", "owner_team", "page_oncall", "probable_cause",
                 "suggested_action", "runbook_url", "related_alerts"],
    "additionalProperties": False
}
Enter fullscreen mode Exit fullscreen mode

The call itself:

bedrock = boto3.client("bedrock-runtime", region_name="sa-east-1",
                       config=Config(read_timeout=120, retries={"max_attempts": 3, "mode": "adaptive"}))

resp = bedrock.invoke_model(
    modelId="global.anthropic.claude-haiku-5-5",
    body=json.dumps({
        "anthropic_version": "bedrock-2023-05-31",
        "max_tokens": 1024,
        "system": SYSTEM,
        "messages": [{"role": "user", "content": "Alertmanager group:\n" + json.dumps(alerts)}],
        "output_config": {"format": {"type": "json_schema", "schema": TRIAGE_SCHEMA}},
    }),
)
body = json.loads(resp["body"].read())
if body["stop_reason"] != "end_turn":
    raise RuntimeError(f"incomplete response: {body['stop_reason']}")
result = json.loads(next(b["text"] for b in body["content"] if b["type"] == "text"))
Enter fullscreen mode Exit fullscreen mode

There's no jsonschema validation and no retry-on-malformed-JSON loop, because you don't need them. The one operational trick: warm up at pod start. Make one dummy call before the readiness probe goes green (I use periodSeconds: 5 and failureThreshold: 30), so grammar compilation happens during the rollout and not on the first real 3 a.m. alert. Run the receiver in parallel with your existing routing (continue: true) for a few weeks and compare decisions.

For agents that act, use strict tools. The enum on namespace and a pattern on the deployment name constrain what the agent can ask for. Ranges like 1 <= replicas <= 20 can't go in the schema, so they stay in code, and execution stays behind human approval.

Gotchas

  • Short read timeouts kill the first request during grammar compilation.
  • Low max_tokens gives you truncated JSON even with structured outputs on.
  • Going past 12 optional properties returns Schema is too complex after up to ~90 seconds.
  • A missing additionalProperties: false in a nested object is an instant 400.
  • Citations plus structured outputs on Claude is a 400.
  • Every structural schema change pays compilation again. Version schemas like API contracts.
  • Check what your SDK actually sends: some strip strict for models they don't know about.

Pricing

No separate charge was announced; you pay the model's tokens. Anthropic's list prices per million tokens: Haiku 5.5 $0.10 input / $0.50 output (prompts up to 100K tokens), Sonnet 5.5 $2 / $10, Opus 5.5 $4 / $20. Check Bedrock's pricing table for your Region and profile. My estimate for 100,000 alert groups a month at ~1,500 input and 150 output tokens on Haiku: about $22.50/month. Batch inference is 50% cheaper for non-urgent work and supports structured outputs. Prompt caching also helps, and Sonnet 5.5 cache reads on Bedrock cost half as much since October 7, 2026.

Region and data residency

From São Paulo, Haiku 5.5 is only available through global cross-Region inference. That's fine for infrastructure alerts. For regulated data, check what a global profile implies first, and keep secrets and customer data out of alert annotations. Some geographic profiles don't support structured outputs for every model, so test with the exact profile your policy requires.

Is it worth it?

If you're using Claude on Bedrock for anything that ends in json.loads: yes. Add output_config, make the schema follow the rules, and delete your validation layer. For agents whose tools touch real systems, strict: true should be the default from today.

My take: the feature has existed since February. What's new is that it no longer depends on the model. While some Claude models supported it and others didn't, every model switch risked breaking parsing, and libraries had to maintain allowlists. With full coverage, moving from Haiku to Sonnet goes back to being a quality and cost decision, not a compatibility one. For platform teams, the pattern generalizes: a small model, a strict schema, enums that map to real decisions, and a human approving anything with impact. It's the most boring and safest way to put AI into an ops workflow, and I mean that as a compliment.

Resources

Top comments (0)