Testing Bedrock's New Feature: Structured Outputs to Enforce Strict JSON Output

Testing Bedrock's New Feature: Structured Outputs to Enforce Strict JSON Output

Amazon Bedrock's Structured Outputs feature enables strict output control through pre-specified JSON schemas. This article introduces implementations across InvokeModel, Converse, and Tool Use APIs using Claude 4.5 Sonnet.
2026.02.05

This page has been translated by machine translation. View original

On February 4, 2026, Structured Outputs became generally available (GA) on Amazon Bedrock.

https://aws.amazon.com/jp/about-aws/whats-new/2026/02/structured-outputs-available-amazon-bedrock/

Until now, even when requesting JSON output, handling symbol intrusions and format issues was essential. With this update, you can now strictly control model output by specifying a JSON schema in advance.

In this article, I'll share my findings on Claude 4.5 Sonnet's behavior and implementation differences across APIs using the Python SDK (boto3).

Test Environment

The latest SDK was required to use Structured Outputs.
I built a Docker container environment to use the latest version as of the time of writing (February 5, 2026).

  • Base Image: python:3.12-slim
  • Python: 3.12.9
  • boto3: 1.42.42

Dockerfile

FROM python:3.12-slim

WORKDIR /app

# Install latest version without using cache
RUN pip install --no-cache-dir boto3 --upgrade

COPY . .

# Check version and run
CMD pip show boto3 | grep Version && python structured_outputs_demo.py

Execution Commands

# Build
docker build -t bedrock-structured-demo .

# Run (passing credentials)
docker run --rm \
  -e AWS_ACCESS_KEY_ID \
  -e AWS_SECRET_ACCESS_KEY \
  -e AWS_SESSION_TOKEN \
  -e AWS_DEFAULT_REGION=ap-northeast-1 \
  bedrock-structured-demo

Version Confirmation Log

From the logs output at container startup, I confirmed that the target version was applied.

Version: 1.42.42

The inference profiles for Claude 4.5 that were available in the Tokyo region as of February 5, 2026 were as follows:

Model Inference Profile ID
Haiku 4.5 (Global) global.anthropic.claude-haiku-4-5-20251001-v1:0
Haiku 4.5 (JP) jp.anthropic.claude-haiku-4-5-20251001-v1:0
Sonnet 4.5 (Global) global.anthropic.claude-sonnet-4-5-20250929-v1:0
Sonnet 4.5 (JP) jp.anthropic.claude-sonnet-4-5-20250929-v1:0
Opus 4.5 (Global) global.anthropic.claude-opus-4-5-20251101-v1:0

In this article, I used Sonnet 4.5 (Global) for testing.

1. Testing with InvokeModel API

First, I tried invoke_model.

Instead of traditional prompt instructions, I specified the JSON structure to output using the output_config parameter.

Implementation Code

I provided CSV text containing "date, product, quantity, price, region" as input and received aggregation results in the specified JSON schema.

import boto3
import json
import os

bedrock = boto3.client("bedrock-runtime", region_name="ap-northeast-1")
MODEL_ID = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"

# Input data
csv_input = """date,product,quantity,price,region
2026-01-15,ノートPC,5,89800,東京
2026-01-15,マウス,20,2980,大阪
2026-01-16,キーボード,8,12800,東京
2026-01-16,モニター,3,45000,福岡"""

# JSON Schema definition
schema = {
    "type": "object",
    "properties": {
        "total_sales": {"type": "integer", "description": "総売上金額"},
        "total_items": {"type": "integer", "description": "総販売数"},
        "by_region": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "region": {"type": "string"},
                    "sales": {"type": "integer"}
                },
                "required": ["region", "sales"],
                "additionalProperties": False
            }
        },
        "top_product": {"type": "string", "description": "最も売上が高い商品"}
    },
    "required": ["total_sales", "total_items", "by_region", "top_product"],
    "additionalProperties": False
}

# Execute request
try:
    response = bedrock.invoke_model(
        modelId=MODEL_ID,
        body=json.dumps({
            "anthropic_version": "bedrock-2023-05-31",
            "max_tokens": 1024,
            "messages": [{"role": "user", "content": f"以下のCSVデータを集計してください:\n\n{csv_input}"}],
            # Specify Structured Outputs here
            "output_config": {"format": {"type": "json_schema", "schema": schema}}
        }),
    )

    result = json.loads(response["body"].read())
    output_text = result["content"][0]["text"]

    # Check if it can be parsed as JSON
    output_json = json.loads(output_text)
    print(json.dumps(output_json, indent=2, ensure_ascii=False))

except Exception as e:
    print(f"Error: {e}")

Execution Result

After running in the Docker environment, I obtained JSON-formatted results by extracting the text field from the response.

{
  "total_sales": 743600,
  "total_items": 36,
  "by_region": [
    {
      "region": "東京",
      "sales": 551400
    },
    {
      "region": "大阪",
      "sales": 59600
    },
    {
      "region": "福岡",
      "sales": 135000
    }
  ],
  "top_product": "ノートPC"
}

Verification Points

  • I confirmed that the structure with objects nested in the by_region array and the types of each field (numeric/string) were output according to the defined schema.

  • The response was returned as a pure JSON string, so no cleaning with regular expressions was needed when executing json.loads() in subsequent processing.

  • In this verification, I confirmed that total_sales and total_items were correctly calculated based on the input CSV data and mapped to the schema fields.

2. Testing with Converse API

Next, I checked how to use the higher-abstraction converse API.
In this case, the parameter position is different, and textFormat within outputConfig is used.

Also, note that a name field is required in the jsonSchema.

Implementation Code (Excerpt)

# Schema definition (using enum)
schema_sentiment = {
    "type": "object",
    "properties": {
        "sentiment": {"type": "string", "enum": ["positive", "negative", "neutral"]}
    },
    "required": ["sentiment"],
    "additionalProperties": False
}

response = bedrock.converse(
    modelId=MODEL_ID,
    messages=[{"role": "user", "content": [{"text": "このサービスは本当に素晴らしい!使いやすくて感動しました。"}]}],
    outputConfig={
        "textFormat": {
            "type": "json_schema",
            "structure": {
                "jsonSchema": {
                    "schema": json.dumps(schema_sentiment), # Must be passed as a string
                    "name": "sentiment_analysis",          # Required
                    "description": "感情分析結果"
                }
            }
        }
    },
)

output = json.loads(response["output"]["message"]["content"][0]["text"])
print(json.dumps(output, indent=2, ensure_ascii=False))

Execution Result

{
  "sentiment": "positive"
}

I confirmed that the value specified in enum (positive) was strictly output.

3. strict Mode in Tool Use

The benefits of Structured Outputs can also be applied to Tool Use (Function Calling).
By adding strict: true to the tool definition, I found that the model can be forced to follow the defined schema when generating tool arguments.

tool_config = {
    "tools": [{
        "toolSpec": {
            "name": "search_products",
            "description": "商品を検索する",
            "inputSchema": {
                "json": {
                    "type": "object",
                    "properties": {
                        "category": {"type": "string", "enum": ["electronics", "books"]},
                        "price_limit": {"type": "integer"}
                    },
                    "required": ["category", "price_limit"],
                    "additionalProperties": False
                }
            }
        }
    }],
    "toolChoice": {"auto": {}}, 
}

I confirmed that schema violations (type mismatches or inclusion of undefined fields) do not occur in tool argument generation based on the input prompt.

Summary

With Amazon Bedrock's Structured Outputs, it has become easier to receive LLM output results in JSON and implement API integrations.

Also, by utilizing enum and required, we can expect to create outputs with less variation.

At present, it's important to be aware of differences in implementation and parameter structures across APIs like InvokeModel and Converse, but this is a powerful tool when handling JSON output with LLMs.

Share this article

FacebookHatena blogX

Related articles