生成AIの活用ノウハウや、 注目の技術に関する記事を掲載
公開

Amazon Bedrock Structured Outputs実装ガイド―JSON出力の変遷とベストプラクティス

生成AI関連

はじめに

生成AIの出力をシステムで扱う場合、自然な文章よりも、決められた項目を持つJSONが必要になることがあります。しかし、プロンプトに「JSONで返してください」と書くだけでは、JSONの前後に説明文やコードフェンスが付く、必須項目が欠ける、想定と異なる型が返るといった問題が残ります。
この問題に対して、これまではプロンプトの工夫やAssistant Prefillで出力を誘導してきました。2026年2月にはAmazon BedrockでStructured Outputsが一般提供され、JSON Schemaに準拠した出力を生成段階で保証できるようになりました(AWS「Structured outputs now available in Amazon Bedrock」)。

Structured Outputsの基本概念と初歩的な実装は、以下の記事で紹介しています。

生成AI関連
Structured Outputsの基本と実践

本記事では、Amazon BedrockでJSONを得る方法の変遷について、次の4つの実装例と実行結果を比較します。

  1. プロンプトだけでJSONを指定する
  2. Assistant Prefillで出力を誘導する
  3. JSON Schema出力形式で最終回答の構造を保証する
  4. strict tool useでツール入力の構造を保証する

3と4の方法は、Amazon BedrockのStructured Outputsを構成する2つの仕組みです。単純な優劣ではなく、保証する対象が異なります。1と2は方式の変遷を理解するための比較対象であり、構造化出力を必要とする新規実装には推奨しません。まずStructured Outputsに対応するモデル、API、エンドポイントを選定することが基本方針です。

JSON出力を安定させる方法の変遷

プロンプトとAssistant Prefillは、モデルにJSON形式での出力を促す方法です。Structured Outputsは、JSON Schemaに沿うよう生成結果そのものを制約します。Structured Outputsには、最終回答に使うJSON Schema出力形式と、ツール入力に使うstrict tool useがあります。

text

プロンプトでJSONを依頼
        ↓
Assistant PrefillでJSONの開始位置を誘導
        ↓
Structured Outputsでスキーマ準拠を保証
        ├─ JSON Schema出力形式:最終回答を保証
        └─ strict tool use:ツール入力を保証
方法 JSON Schemaによる保証 対象 位置付け
プロンプトだけ なし 最終回答 比較・検証用。構造化出力には原則として使わない
Assistant Prefill なし 最終回答の書き始め 既存実装向け。新規実装には使わない
JSON Schema出力形式 あり(正常終了時) 最終回答 情報抽出、分類、APIレスポンスの第一候補
strict tool use あり(正常終了時) ツール名と入力 関数呼び出し、エージェント、外部API連携

ここでいう「保証あり」は、正常終了した出力がJSON Schemaに準拠するよう制約されるという意味です。リクエストが常に有効なJSONで完了することを100%保証するものではなく、最大トークン数への到達や、同じ内容を繰り返す出力ループなどによってJSONが未完了になる可能性があります。出力ループの実例と、トークン上限、再試行、監視による対策は、以下の記事で紹介しています。

生成AI関連
生成AIの出力ループをどう防ぐ?想定外のコスト増を抑えるための対策

Amazon Bedrockの公式ドキュメントでも、JSON Schema出力形式とstrict tool useは「2つの補完的な仕組み」と説明されています。同じリクエストで併用することも可能です。

JSON出力を安定させる方法とStructured Outputsの2つの仕組み

JSON Schema出力形式とstrict tool useは、Structured Outputsの中で異なる対象を保証する

以下では、Claude Haiku 4.5に同じ問い合わせを送り、4つの方法を比較します。

1. プロンプトだけでJSONを指定する

最初は、プロンプトに項目、候補値、出力形式を文章で記載する方法です。Converse APIへ通常のメッセージを送り、モデルが返したテキストをそのまま表示します。

prompt_only.py

import os

import boto3
from dotenv import load_dotenv

PROMPT = """
次の問い合わせをJSONに変換してください。説明文は付けないでください。

項目:
- category: 障害、請求、契約、その他のいずれか
- priority: high、medium、lowのいずれか
- summary: 問い合わせ内容の要約

問い合わせ:
今朝から管理画面へログインできません。
業務に影響するため、至急確認をお願いします。
"""


def main() -> None:
    load_dotenv()
    client = boto3.client("bedrock-runtime", region_name=os.environ["AWS_REGION"])
    response = client.converse(
        modelId=os.environ["BEDROCK_MODEL_ID"],
        messages=[{"role": "user", "content": [{"text": PROMPT}]}],
        inferenceConfig={"maxTokens": 500, "temperature": 0},
    )

    print(response["output"]["message"]["content"][0]["text"])


if __name__ == "__main__":
    main()

今回の実行では項目と値は意図どおりでしたが、「説明文は付けない」と指示してもJSONがMarkdownのコードブロックで囲まれました。

プロンプトだけでJSONを指定した実行結果

JSONは得られたものの、前後にコードフェンスが付いている

この出力をjson.loads()へ直接渡すと解析に失敗します。コードフェンスを取り除けば解析できますが、別の入力や実行では説明文、項目不足、候補外の値が返る可能性もあります。プロンプトは形式を「依頼」しているだけであり、JSONの構文やスキーマへの準拠を保証していません。
そのため、構造化データを後続処理へ渡す用途では、この方式を新規採用しないことを推奨します。まずJSON Schema出力形式に対応するモデルを選定し、利用するAPIやエンドポイントも含めてStructured Outputsを利用できる構成へ変更できないかを検討します。

2. Assistant Prefillで出力を誘導する

Assistant Prefillは、Structured Outputs登場前の既存実装を理解するために紹介する方式であり、新規実装には推奨しません。モデルの回答欄を途中まで書いた状態にして、その続きを生成させる方法です。次の例では、assistantメッセージとして、コードフェンスと{をあらかじめ指定しています。さらにコードフェンスの終了記号を停止文字列として設定し、JSONの後に文章が続くことを抑えます。

assistant_prefill.py

import json
import os

import boto3
from dotenv import load_dotenv

PROMPT = """
次の問い合わせを、category、priority、summaryを持つJSONに変換してください。
categoryは障害、請求、契約、その他のいずれか、
priorityはhigh、medium、lowのいずれかにしてください。

問い合わせ:
今朝から管理画面へログインできません。
業務に影響するため、至急確認をお願いします。
"""


def main() -> None:
    load_dotenv()
    client = boto3.client("bedrock-runtime", region_name=os.environ["AWS_REGION"])
    body = {
        "anthropic_version": "bedrock-2023-05-31",
        "max_tokens": 500,
        "temperature": 0,
        "messages": [
            {"role": "user", "content": PROMPT},
            {"role": "assistant", "content": "```json\n{"},
        ],
        "stop_sequences": ["```"],
    }

    response = client.invoke_model(
        modelId=os.environ["BEDROCK_MODEL_ID"],
        body=json.dumps(body),
    )
    result = json.loads(response["body"].read())
    json_text = "{" + result["content"][0]["text"]

    print("stop_reason:", result["stop_reason"], "\n")
    print(json.dumps(json.loads(json_text), ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()

実行結果ではstop_reasonstop_sequenceとなり、コードフェンスより前で生成が停止しました。事前に与えた{を連結すると、JSONとして解析できます。

Assistant PrefillでJSON出力を誘導した実行結果

今回の入力では、Assistant Prefillと停止文字列により、説明文を含まないJSONを取得できた

この結果だけを見ると、Assistant Prefillでも十分に安定しているように見えるかもしれません。しかし、今回の例は、項目数が少なく入力も短い単純なケースを、指示追従性能の高い現在のモデルで実行した一例にすぎません。高性能なモデルでは、プロンプトだけ、またはAssistant Prefillでも、同様の入力に対して意図どおりのJSONが得られる場合があります。
重要なのは、「今回JSONとして解析できたこと」と「正常終了した出力のスキーマ準拠が仕組みとして保証されること」は別だという点です。入力の長さや曖昧さ、項目数、ネストの深さ、候補値、モデルの更新などによって結果は変化し得ます。単発の成功例だけを根拠に本番採用を判断せず、実データを想定した正常系、境界値、異常系で反復評価する必要があります。

Assistant PrefillもJSON Schemaによる保証ではありません。評価時に高い成功率が得られた場合でも、JSONの構文が崩れる、必須項目が欠ける、型が異なる、候補外の値が返るといった可能性に備え、受信後のパース、バリデーション、再試行が必要です。Structured Outputsとの違いは、この実行結果が成功したかどうかではなく、生成時にスキーマ準拠を制約できるかどうかにあります。
ただし、Assistant Prefillは、すべての現行Claudeモデルで利用できるわけではありません。AnthropicのMessages APIドキュメントによると、Assistant PrefillはClaude 4.6以降でサポートされず、指定するとエラーになります。
新規実装では、まずStructured Outputsに対応するモデルへ変更できるかを検討してください。モデルの変更が難しい既存システムでも、Assistant Prefillを継続利用する前に、APIやエンドポイントの変更によってStructured Outputsへ移行できないかを確認します。いずれも変更できない場合に限り、JSONの破損やスキーマ違反が起こり得ることを受け入れ、受信後のパース、バリデーション、再試行、監視を組み込んだ上で暫定的に利用します。

Error Message

botocore.errorfactory.ValidationException: An error occurred (ValidationException) when calling the InvokeModel operation: This model does not support assistant message prefill. The conversation must end with a user message.

3. JSON Schema出力形式で最終回答を保証する

ここからがAmazon BedrockのStructured Outputsです。JSON Schema出力形式では、モデルの最終回答が指定したスキーマに準拠するよう、生成可能なトークンが制約されます。
今回は、同じ問い合わせを使ってPydanticモデルからJSON Schemaを生成し、実行結果を確認します。

json_schema_output.py

import json
import os
from typing import Literal

import boto3
from dotenv import load_dotenv
from pydantic import BaseModel, ConfigDict, Field


class SupportTicket(BaseModel):
    model_config = ConfigDict(extra="forbid")

    category: Literal["障害", "請求", "契約", "その他"] = Field(
        description="問い合わせの分類"
    )
    priority: Literal["high", "medium", "low"] = Field(description="対応優先度")
    summary: str = Field(description="問い合わせ内容の要約")

def main() -> None:
    load_dotenv()
    client = boto3.client("bedrock-runtime", region_name=os.environ["AWS_REGION"])
    prompt = (
        "次の問い合わせを分類・要約してください。\n\n"
        "今朝から管理画面へログインできません。"
        "業務に影響するため、至急確認をお願いします。"
    )

    response = client.converse(
        modelId=os.environ["BEDROCK_MODEL_ID"],
        messages=[{"role": "user", "content": [{"text": prompt}]}],
        inferenceConfig={"maxTokens": 500},
        outputConfig={
            "textFormat": {
                "type": "json_schema",
                "structure": {
                    "jsonSchema": {
                        "schema": json.dumps(
                            SupportTicket.model_json_schema(), ensure_ascii=False
                        ),
                        "name": "support_ticket",
                    }
                },
            }
        },
    )

    if response["stopReason"] != "end_turn":
        raise RuntimeError(f"生成が正常終了しませんでした: {response['stopReason']}")

    output_text = response["output"]["message"]["content"][0]["text"]
    ticket = SupportTicket.model_validate_json(output_text)
    print(ticket.model_dump_json(indent=2))


if __name__ == "__main__":
    main()

実行結果は、コードフェンスを含まないJSONです。categorypriorityLiteralで指定した候補に限定され、3項目がそろっています。

JSON Schema出力形式で構造を保証した実行結果

Pydanticモデルから生成したJSON Schemaに準拠する最終回答を取得できた

実装の要点は、次の3点です。

  • ConfigDict(extra="forbid")により、生成されるJSON SchemaのadditionalPropertiesをfalseにする
  • SupportTicket.model_json_schema()でクラス定義からJSON Schemaを生成し、outputConfig.textFormatに直接指定する
  • 受け取ったJSONをSupportTicket.model_validate_json()でPydanticモデルに変換する

model_json_schema()はクラスからスキーマを生成するメソッドです。一方、最後のticket.model_dump_json()は、検証済みのインスタンスをJSON文字列に変換します。用途が異なるため、リクエストではmodel_json_schema()、出力時にはmodel_dump_json()を使用しています。
Structured Outputsが保証するのは、生成が正常終了した場合に、出力が対応するJSON Schemaに構造的に準拠することです。分類や要約の内容が業務上正しいことまでは保証しないため、内容の評価や業務ルールの検証は別途必要です。また、拒否、最大トークン数への到達、出力ループなどではJSONが未完了になり、スキーマに準拠しない場合があります。そのため、AWSはすべてのレスポンスでstopReasonを確認するよう案内しています(AWS Machine Learning Blog)。

4. strict tool useでツール入力を保証する

Structured Outputsのもう一つの仕組みがstrict tool useです。JSON Schema出力形式がモデルの最終回答を保証するのに対し、strict tool useはモデルが生成するツール名と入力をツール定義に準拠させます。
次の例では、問い合わせを登録するcreate_support_ticketツールを定義します。実際の登録処理は実行せず、モデルが生成したtoolUseブロックの入力だけを確認します。

strict_tool_use.py

import os
from typing import Literal

import boto3
from dotenv import load_dotenv
from pydantic import BaseModel, ConfigDict, Field


class SupportTicket(BaseModel):
    model_config = ConfigDict(extra="forbid")

    category: Literal["障害", "請求", "契約", "その他"] = Field(
        description="問い合わせの分類"
    )
    priority: Literal["high", "medium", "low"] = Field(description="対応優先度")
    summary: str = Field(description="問い合わせ内容の要約")


def main() -> None:
    load_dotenv()
    client = boto3.client("bedrock-runtime", region_name=os.environ["AWS_REGION"])
    prompt = (
        "次の問い合わせをcreate_support_ticketツールで登録してください。\n\n"
        "今朝から管理画面へログインできません。"
        "業務に影響するため、至急確認をお願いします。"
    )

    response = client.converse(
        modelId=os.environ["BEDROCK_MODEL_ID"],
        messages=[{"role": "user", "content": [{"text": prompt}]}],
        inferenceConfig={"maxTokens": 500},
        toolConfig={
            "tools": [
                {
                    "toolSpec": {
                        "name": "create_support_ticket",
                        "description": "問い合わせをサポート窓口へ登録する",
                        "inputSchema": {"json": SupportTicket.model_json_schema()},
                        "strict": True,
                    }
                }
            ],
            "toolChoice": {"any": {}},
        },
    )

    tool_use = next(
        block["toolUse"]
        for block in response["output"]["message"]["content"]
        if "toolUse" in block
    )
    print("tool:", tool_use["name"], "\n")
    ticket = SupportTicket.model_validate(tool_use["input"])
    print(ticket.model_dump_json(indent=2))


if __name__ == "__main__":
    main()

実行結果ではcreate_support_ticketが選択され、入力として3項目が返りました。

strict tool useでツール入力の構造を保証した実行結果

strict tool useにより、ツール入力がPydanticモデルから生成したスキーマに準拠した

inputSchema.jsonにPydanticのJSON Schemaを直接指定し、strictTrueにする部分が中心です。toolChoiceanyは、定義したツールのいずれかを必ず呼び出す指定です。
ここで保証されるのは、ツール呼び出しに含まれる名前と入力です。外部APIやデータベースへの登録が自動的に実行されるわけではありません。アプリケーション側でtoolUseを受け取り、権限確認や業務ルールの検証を行った上で、実際の関数を呼び出します。

Structured Outputsの選び方と実装上の注意点

最終回答をJSONとして受け取りたい場合は、JSON Schema出力形式が第一候補です。情報抽出、分類、APIレスポンスなど幅広い用途にそのまま利用できるため、一般的な「JSONを安定して返してほしい」という要件に最も合います。

ただし、JSON Schema出力形式はすべてのモデルで利用できるわけではありません。AWSの公式ドキュメントは、対応状況をモデルごとに確認するよう案内しています。たとえばClaude向けの公式ドキュメントでは、執筆時点のStructured Outputs対応モデルがClaude Sonnet 4.5、Claude Haiku 4.5、Claude Opus 4.5、Claude Opus 4.6に限定されています。
また、対応モデルであっても、bedrock-mantleエンドポイントのAnthropic Messages APIではStructured Outputsを利用できません。Claudeで利用する場合は、bedrock-runtimeエンドポイントのConverse APIまたはInvokeModel APIを使用します。

生成AI関連
Amazon Bedrock Mantleとは?OpenAI・Anthropic互換APIで変わるB…

現在採用しているモデルがJSON Schema出力形式に対応していない場合は、代替手段としてTool useを検討する前に、Structured Outputs対応モデルへの変更を検討します。モデルを変更できない場合も、利用するAPIやエンドポイントを変更してJSON Schema出力形式を利用できないかを確認します。
モデル、API、エンドポイントのいずれも変更できず、そのモデルがTool useには対応している場合に限り、構造化結果を受け取るためのツールを定義し、toolChoiceで呼び出しを必須にする方法が代替案になります。このケースでは実際に外部機能を呼び出す必要がないにもかかわらず、ツール入力を構造化結果として取り出す実装になるため、処理の意図が分かりにくくなります。

一方、モデルに外部機能を選択させ、その引数を生成させる場合はstrict tool useが本来の用途に適しています。なお、JSON Schema出力形式に非対応のモデルが、strict tool useにも対応しているとは限りません。通常のTool useしか利用できない場合はスキーマ準拠が保証されないため、受信後のバリデーション、再試行、監視が必要です。こうした代替方式はリスクを把握し、許容できると判断した場合に限って暫定的に採用し、Structured Outputs対応構成への移行計画も併せて用意します。

状況 方針
最終回答を決められたJSONで受け取りたい JSON Schema出力形式を第一候補にする
現在のモデルや実行経路がJSON Schema出力形式に対応していない 対応モデル、API、エンドポイントへの変更を検討する
ツールや関数へ渡す引数を保証したい strict tool useを選ぶ
最終回答とツール入力の両方を保証したい 2つを併用する
モデルを変更できず、通常のTool useのみ利用できる 原則非推奨。リスクを許容できる場合のみ受信後の検証を行う
JSON Schema出力形式とTool useの両方に対応していない 原則として採用しない。継続が不可避な場合のみ暫定利用する

Structured Outputsで使用できるのはJSON Schema Draft 2020-12のサブセットです。たとえば文字列のminLengthmaxLength、数値のminimummaximum、再帰スキーマ、外部$refはサポートされません。オブジェクトのadditionalPropertiesfalseにする必要があります。新しいスキーマは初回に文法がコンパイルされ、完了まで数分かかる場合があります。正常にコンパイルされた文法は、最初のアクセスから24時間キャッシュされます。対応状況は更新されるため、実装時はAmazon BedrockのStructured Outputs公式ドキュメントをご確認ください。

まとめ

Amazon BedrockでJSON出力を安定させる方法は、プロンプトによる依頼やAssistant Prefillによる誘導から、JSON Schemaに準拠するStructured Outputsへ進化しました。

  • プロンプトだけでは、JSONの構文や項目を保証できない
  • Assistant Prefillは出力の開始位置を誘導できるが、スキーマ準拠は保証せず、Claude 4.6以降では利用できない
  • JSON Schema出力形式は、モデルの最終回答を指定した構造へ制約する
  • strict tool useは、モデルが生成するツール名と入力をツール定義へ準拠させる
  • 構造化出力には、JSON Schema出力形式に対応するモデル、API、エンドポイントを優先して選定する
  • 非対応モデルを変更できない場合の代替方式は、リスクを許容できる場合に限って暫定的に利用する

まず利用するモデルがJSON Schema出力形式に対応しているかを確認し、非対応であればモデル、API、エンドポイントの変更を検討します。プロンプト、Assistant Prefill、通常のTool useによる代替は標準的な選択肢とせず、モデル、API、エンドポイントを変更できず、かつリスクを許容できる場合に限って、検証や監視を伴う暫定策として利用します。

生成AI活用支援サービスのご紹介

Tech Funでは、お客様のフェーズに合わせ、生成AI活用に向けた様々な支援をご提供しています。

  1. 無料診断パック:業務・プロセスの現状を無料で診断し、生成AI活用の可能性をレポートします。
  2. 検証(PoC)パック:診断で有効性が確認された業務を対象に、プロトタイプ構築を支援します。
  3. コンサルティングサービス:生成AI導入戦略の策定から運用体制構築までを包括的に支援します。
  4. Plain RAG:社内文書をもとに回答し、社内問い合わせを効率化するAIチャットパッケージです。

生成AIに限らず、Web・業務システム開発やインフラ設計など、技術領域を問わずご相談を承っています。「何から始めれば良いか分からない」という段階でも構いませんので、ぜひお気軽にお問い合わせください。

執筆・編集

Tech Fun Magazine R&Dチーム
Tech Funの生成AI研究に携わるエンジニアが、最新のAIモデル動向やプロンプト設計、実業務への応用手法など、生成AIに特化した知見を執筆・編集しています。
モデル評価や業務シナリオに応じたAI活用設計など、日々のR&D活動で得られる実践的なノウハウをわかりやすく紹介します。

ARTICLE
生成AI関連記事一覧

生成AI関連

Amazon Bedrock Structured Outp…

生成AI関連

ChatGPT Workとは?Chat・Codexとの違いを…

生成AI関連

トークナイザーとは?生成AIサービスごとの違いを比較してみた

生成AI関連

Codex CLIの/sideと/forkは何が違う? 会話…

生成AI関連

生成AIの出力ループをどう防ぐ?想定外のコスト増を抑えるため…

生成AI関連

Claudeのプラグインとは?DataプラグインをChatと…

生成AI関連

ChatGPT Sitesとは?データを保存するWebアプリ…

生成AI関連

コーディングエージェントを従量課金で走らせる前に ― トーク…

生成AI関連

Amazon Bedrock Mantleとは?OpenAI…

記事一覧を見る