Bedrockの新機能 Structured Outputs (構造化出力)でJSON出力を厳格化してみた
2026年2月4日、Amazon BedrockにてStructured Outputs(構造化出力)が一般提供(GA)開始されました。
これまではJSON出力を指示しても、記号の混入や形式不備への対応が不可欠でした。今回のアップデートにより、JSONスキーマを事前指定することで、モデルの出力を厳格に制御できるようになりました。
今回、Python SDK (boto3) を用いて、Claude 4.5 Sonnetでの挙動と、APIごとの実装差異について確認する機会がありましたので、紹介します。
検証環境
Structured Outputsを利用するには最新のSDKが必要でした。
執筆時点(2026年2月5日)の最新版を利用するため、Dockerコンテナで検証環境を構築しました。
- ベースイメージ: python:3.12-slim
- Python: 3.12.9
- boto3: 1.42.42
Dockerfile
FROM python:3.12-slim
WORKDIR /app
# キャッシュを使わず最新版をインストール
RUN pip install --no-cache-dir boto3 --upgrade
COPY . .
# バージョン確認と実行
CMD pip show boto3 | grep Version && python structured_outputs_demo.py
実行コマンド
# ビルド
docker build -t bedrock-structured-demo .
# 実行(クレデンシャルを渡す)
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: 1.42.42
東京リージョンで動作確認できたClaude 4.5の推論プロファイル、2026年2月5日の記事執筆時点では以下の通りでした。
| モデル | 推論プロファイル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 |
本記事では、この中から Sonnet 4.5 (Global) を使用して検証を行いました。
1. InvokeModel APIでの検証
まず invoke_model を試しました。
従来のプロンプト指示ではなく、output_config パラメータで出力するJSON構造を指定しました。
実装コード
入力として「日付、商品、数量、単価、地域」を含むCSVテキストを与え、集計結果を指定したJSONスキーマで受け取ります。
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"
# 入力データ
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定義
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
}
# リクエスト実行
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}"}],
# ここでStructured Outputsを指定
"output_config": {"format": {"type": "json_schema", "schema": schema}}
}),
)
result = json.loads(response["body"].read())
output_text = result["content"][0]["text"]
# 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}")
実行結果
上記のDocker環境で実行後、レスポンスから text フィールドを抽出する事で、JSON形式の結果が得られました。
{
"total_sales": 743600,
"total_items": 36,
"by_region": [
{
"region": "東京",
"sales": 551400
},
{
"region": "大阪",
"sales": 59600
},
{
"region": "福岡",
"sales": 135000
}
],
"top_product": "ノートPC"
}
検証での確認ポイント
-
by_region配列の中にオブジェクトがネストされる構造や、各フィールドの型(数値/文字列)が、定義したスキーマ通りに出力されていることを確認しました。 -
レスポンスは純粋なJSON文字列として返却されたため、後続の処理で
json.loads()を実行する際も正規表現などによるクリーニングが不要でした。 -
今回の確認では、入力CSVデータに基づき、
total_sales(総売上)やtotal_items(総数)が正しく計算され、スキーマのフィールドにマッピングされていることを確認できました。
2. Converse APIでの検証
次に、より抽象度の高い converse APIを使用する場合の記述方法を確認しました。
こちらではパラメータ位置が異なり、outputConfig 内の textFormat を使用しました。
また、jsonSchema 内に name フィールドが必須となる点に注意が必要でした。
実装コード(抜粋)
# スキーマ定義(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), # 文字列として渡す必要あり
"name": "sentiment_analysis", # 必須
"description": "感情分析結果"
}
}
}
},
)
output = json.loads(response["output"]["message"]["content"][0]["text"])
print(json.dumps(output, indent=2, ensure_ascii=False))
実行結果
{
"sentiment": "positive"
}
enum で指定した値(positive)が厳格に出力されていることを確認しました。
3. Tool Useにおける strict モード
Structured Outputsの恩恵は、Tool Use(Function Calling)にも適用可能です。
ツール定義に strict: true を追加することで、モデルがツール引数を生成する際、定義したスキーマに強制的に従わせることができました。
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": {}},
}
入力されたプロンプトに基づき、Tool Useの引数生成においてもスキーマ違反(型不一致や未定義フィールドの混入)が発生しないことを確認できました。
まとめ
Amazon BedrockのStructured Outputsにより、LLM出力結果をJSONで受け取り、API連携などの実装が容易になりました。
また、enum や required を活用することで、揺れの少ない出力の作成も期待できるようになりました。
現時点で、InvokeModel、Converse とAPI毎の実装、パラメータ構造の違いなどがあるため留意が必要ですが、LLMでJSON出力を扱う場合に、有力な手段として活用ください。






