[Update] Structured Outputs now supported on Amazon Bedrock
This page has been translated by machine translation. View original
Introduction
Hello, I'm Jinno from the consulting department who loves supermarkets.
Have you ever struggled with JSON parsing failures when handling LLM outputs in your applications? Even when you ask in your prompt "please output in JSON format," you might get extra explanatory text or slightly broken formatting...
Previously, I wrote an article about using the Structured Output feature in Strands Agents to structure outputs.
With a recent update, Structured Outputs is now supported directly in Bedrock itself. (Note that it's "outputs" in plural...)
Let's try it out right away!!
Structured Outputs
Let me explain Structured Outputs again.
This feature ensures that model outputs comply with a predefined JSON schema.
The official documentation states:
Structured outputs is a capability on Amazon Bedrock that ensures model responses conform to user-defined JSON schemas and tool definitions, reducing the need for custom parsing and validation mechanisms in production AI deployments.
Translating the benefits from the official documentation:
- Schema compliance guarantee: Eliminates errors and retry loops that occur in prompt-based approaches
- Reduces development complexity: No need for custom parsing and validation logic
- Reduces operational costs: Fewer failed requests and retries
- Improves production reliability: Confidently deploy AI applications that require predictable and machine-readable outputs
Previously, we had to implement our own logic to parse JSON, validate it, retry if it failed, and so on. It's great that Structured Outputs now supports these operations.
As a side note, Anthropic API has supported Structured Outputs for some time. This update means that the feature is now available when using models like Claude through Amazon Bedrock.
Since it was already available through frameworks like Strands Agents, I also realized that it wasn't directly available until now.
Two Usage Methods
There are two ways to use Structured Outputs:
JSON Schema output format
This method makes the entire model response conform to a specified JSON schema. It's suitable for cases where you want to directly obtain structured data, such as data extraction or report generation.
This is easy to imagine. It simply structures the response in the specified format.
Strict Tool Use
This method leverages Structured Outputs in tool use (Function Calling).
For example, if you want a weather tool to only accept "celsius" or "fahrenheit" for the unit parameter:
{
"name": "get_weather",
"strict": True, # This is the key addition
"inputSchema": {
"json": {
"type": "object",
"properties": {
"location": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
...
}
}
}
Adding strict: true ensures that the tool call arguments strictly conform to the schema. In the example above, unit will always be either "celsius" or "fahrenheit".
Differences between the two methods
To summarize, use JSON Schema output format when you want to structure the model's output itself, and Strict Tool Use when you want to structure the arguments for tool calls. Choose according to your needs.
Supported Models and APIs
The supported models at the time of writing are:
Anthropic
Claude Haiku 4.5 (
anthropic.claude-haiku-4-5-20251001-v1:0)Claude Sonnet 4.5 (anthropic.claude-sonnet-4-5-20250929-v1:0)Claude Opus 4.5 (anthropic.claude-opus-4-5-20251101-v1:0)Qwen
Qwen3 235B A22B 2507 (
qwen.qwen3-235b-a22b-2507-v1:0)Qwen3 32B (dense) (qwen.qwen3-32b-v1:0)Qwen3-Coder-30B-A3B-Instruct (qwen.qwen3-coder-30b-a3b-v1:0)Qwen3 Coder 480B A35B Instruct (qwen.qwen3-coder-480b-a35b-v1:0)Qwen3 Next 80B A3B (qwen.qwen3-next-80b-a3b)Qwen3 VL 235B A22B (qwen.qwen3-vl-235b-a22b)OpenAI
gpt-oss-120b (
openai.gpt-oss-120b-1:0)gpt-oss-20b (openai.gpt-oss-20b-1:0)GPT OSS Safeguard 120B (openai.gpt-oss-safeguard-120b)GPT OSS Safeguard 20B (openai.gpt-oss-safeguard-20b)DeepSeek
DeepSeek-V3.1 (
deepseek.v3-v1:0)Gemma 3 12B IT (
google.gemma-3-12b-it)Gemma 3 27B PT (google.gemma-3-27b-it)MiniMax
MiniMax M2 (
minimax.minimax-m2)Mistral AI
Magistral Small 2509 (
mistral.magistral-small-2509)Ministral 3B (mistral.ministral-3-3b-instruct)Ministral 3 8B (mistral.ministral-3-8b-instruct)Ministral 14B 3.0 (mistral.ministral-3-14b-instruct)Mistral Large 3 (mistral.mistral-large-3-675b-instruct)Voxtral Mini 3B 2507 (mistral.voxtral-mini-3b-2507)Voxtral Small 24B 2507 (mistral.voxtral-small-24b-2507)Moonshot AI
Kimi K2 Thinking (
moonshot.kimi-k2-thinking)NVIDIA
NVIDIA Nemotron Nano 12B v2 VL BF16 (
nvidia.nemotron-nano-12b-v2)NVIDIA Nemotron Nano 9B v2 (nvidia.nemotron-nano-9b-v2)
Supported APIs:
- Converse API / ConverseStream API
- InvokeModel API / InvokeModelWithResponseStream API
Cross-Region inference and Batch inference are also supported.
Prerequisites
The testing environment for this article is as follows:
- Python 3.12
- uv
- AWS CLI configured (with access permissions to Bedrock)
- Model used: Claude Haiku 4.5 (
us.anthropic.claude-haiku-4-5-20251001-v1:0)
Setup
Let's set up the project:
uv init
Install dependencies:
uv add boto3
Now we're ready to go!
Implementation
Let's look at actual implementations using Structured Outputs.
I'll implement two patterns and verify their behavior later.
JSON Schema output format Pattern
First, let's try the JSON Schema output format pattern.
I've implemented a process to extract project information from unstructured text.
"""
Amazon Bedrock Structured Outputs - JSON Schema Pattern
Use Converse API to make model outputs conform to a JSON schema
"""
import boto3
import json
# Create Bedrock Runtime client
bedrock_runtime = boto3.client("bedrock-runtime", region_name="us-east-1")
# Model ID to use (Claude Haiku 4.5)
MODEL_ID = "us.anthropic.claude-haiku-4-5-20251001-v1:0"
# Sample unstructured text
SAMPLE_TEXT = """
昨日の会議で新しいプロジェクトについて話し合いました。
プロジェクト名は「Operation Phoenix」で、来月の15日から開始予定です。
担当者は田中さん、鈴木さん、佐藤さんの3名で、予算は500万円です。
主な目標はレガシーシステムの刷新で、期間は6ヶ月を見込んでいます。
"""
# JSON schema defining the data structure to extract
json_schema = {
"type": "object",
"properties": {
"project_name": {
"type": "string",
"description": "プロジェクトの名称"
},
"start_date": {
"type": "string",
"description": "プロジェクトの開始予定日"
},
"team_members": {
"type": "array",
"items": {"type": "string"},
"description": "プロジェクトメンバーのリスト"
},
"budget": {
"type": "string",
"description": "プロジェクトの予算"
},
"objective": {
"type": "string",
"description": "プロジェクトの主な目標"
},
"duration": {
"type": "string",
"description": "プロジェクトの予定期間"
}
},
"required": ["project_name", "start_date", "team_members", "budget", "objective", "duration"],
"additionalProperties": False
}
def extract_structured_data():
"""Extract structured data from unstructured text"""
response = bedrock_runtime.converse(
modelId=MODEL_ID,
messages=[
{
"role": "user",
"content": [
{
"text": f"以下のテキストから情報を抽出してください:\n\n{SAMPLE_TEXT}"
}
]
}
],
# Structured Outputs configuration
outputConfig={
"textFormat": {
"type": "json_schema",
"structure": {
"jsonSchema": {
"schema": json.dumps(json_schema),
"name": "project_extraction",
"description": "プロジェクト情報の抽出"
}
}
}
}
)
# Get text from response
output_text = response["output"]["message"]["content"][0]["text"]
return json.loads(output_text)
if __name__ == "__main__":
print("=" * 60)
print("Amazon Bedrock Structured Outputs - JSON Schema パターン")
print("=" * 60)
print(f"\n使用モデル: {MODEL_ID}")
print(f"\n--- 入力テキスト ---\n{SAMPLE_TEXT}")
print("\n--- 抽出結果 ---")
result = extract_structured_data()
print(json.dumps(result, indent=2, ensure_ascii=False))
In the Converse API, JSON Schema output format is specified in outputConfig.textFormat. The schema is passed as a string in structure.jsonSchema.schema.
Don't forget to specify required and additionalProperties: false.
It's simple - just specify the schema for the structure you want to receive and send the request.
Strict Tool Use Pattern
Next, let's try the Strict Tool Use pattern.
I've implemented a comparison test with and without strict: true to see if there's any difference in the results.
"""
Comparison test with and without strict: true
"""
import boto3
import json
bedrock_runtime = boto3.client("bedrock-runtime", region_name="us-east-1")
MODEL_ID = "us.anthropic.claude-haiku-4-5-20251001-v1:0"
def create_tool(strict: bool):
"""Create tool definition with strict flag"""
tool = {
"toolSpec": {
"name": "get_weather",
"description": "指定した都市の現在の天気を取得する",
"inputSchema": {
"json": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "天気を取得する都市名"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度の単位"
}
},
"required": ["location", "unit"],
"additionalProperties": False
}
}
}
}
if strict:
tool["toolSpec"]["strict"] = True
return tool
def call_weather_tool(strict: bool, user_message: str):
"""Call the weather tool"""
response = bedrock_runtime.converse(
modelId=MODEL_ID,
messages=[
{
"role": "user",
"content": [{"text": user_message}]
}
],
toolConfig={
"tools": [create_tool(strict)],
"toolChoice": {"tool": {"name": "get_weather"}}
}
)
return response["output"]["message"]["content"][0]["toolUse"]["input"]
if __name__ == "__main__":
print("=" * 60)
print("strict: true ありなしの比較")
print("=" * 60)
# Requests using expressions not in the enum
test_messages = [
"東京の天気をCで教えて",
"パリの気温をケルビンで知りたい",
"ニューヨークの天気、普通の単位で",
]
for msg in test_messages:
print(f"\n【リクエスト】{msg}")
print("-" * 40)
print("strict: false(指定なし):")
try:
result = call_weather_tool(strict=False, user_message=msg)
print(f" unit = {result.get('unit')}")
except Exception as e:
print(f" エラー: {e}")
print("strict: true:")
try:
result = call_weather_tool(strict=True, user_message=msg)
print(f" unit = {result.get('unit')}")
except Exception as e:
print(f" エラー: {e}")
I've set an enum constraint on unit (only "celsius" or "fahrenheit" allowed).
Adding strict: True guarantees that one of these two values will always be returned.
Verification
Let's run the scripts and check the results.
JSON Schema output format Pattern
uv run structured_output_json_schema.py
============================================================
Amazon Bedrock Structured Outputs - JSON Schema パターン
============================================================
使用モデル: us.anthropic.claude-haiku-4-5-20251001-v1:0
--- 入力テキスト ---
昨日の会議で新しいプロジェクトについて話し合いました。
プロジェクト名は「Operation Phoenix」で、来月の15日から開始予定です。
担当者は田中さん、鈴木さん、佐藤さんの3名で、予算は500万円です。
主な目標はレガシーシステムの刷新で、期間は6ヶ月を見込んでいます。
--- 抽出結果 ---
{
"project_name": "Operation Phoenix",
"start_date": "来月の15日",
"team_members": [
"田中さん",
"鈴木さん",
"佐藤さん"
],
"budget": "500万円",
"objective": "レガシーシステムの刷新",
"duration": "6ヶ月"
}
Wow, information has been cleanly extracted from the unstructured text!!
I can confirm that it's output in JSON format according to the defined schema.
Strict Tool Use Pattern
uv run strict_comparison.py
============================================================
strict: true ありなしの比較
============================================================
【リクエスト】東京の天気をCで教えて
----------------------------------------
strict: false(指定なし):
unit = celsius
strict: true:
unit = celsius
【リクエスト】パリの気温をケルビンで知りたい
----------------------------------------
strict: false(指定なし):
unit = celsius
strict: true:
unit = celsius
【リクエスト】ニューヨークの天気、普通の単位で
----------------------------------------
strict: false(指定なし):
unit = celsius
strict: true:
unit = fahrenheit
As a result, there wasn't much difference in most cases. The model is smart enough to follow the schema even without strict: true. The interesting difference is only with New York. It seems like the model interpreted "normal unit" for New York as fahrenheit.
Rather than just happening to match, the guaranteed compliance is the valuable part. Being able to eliminate validation logic and retry processing by ensuring schema compliance simplifies things. It would be interesting to test this with more complex schemas as well.
Additional Notes
Explicit specification of additionalProperties is required
additionalProperties is a JSON Schema property that controls whether additional properties not defined in the schema are allowed. Setting it to false means only properties defined in the schema are allowed.
If you don't specify additionalProperties, you'll get an error like this:
ValidationException: An error occurred (ValidationException) when calling the Converse operation:
The model returned the following errors: tools.0.custom: For 'object' type, 'additionalProperties' must be explicitly set to false
Explicit specification of additionalProperties is required. Don't forget to set it.
Property differences between APIs and providers
In this article, I used the Converse API, but property names differ when using InvokeModel API or depending on the model provider.
For JSON Schema output format
| API | Property |
|---|---|
| Converse API | outputConfig.textFormat |
| InvokeModel (Anthropic Claude) | output_config.format |
| InvokeModel (Open-weight models) | response_format |
For Strict Tool Use
| API | Property |
|---|---|
| Converse API | toolSpec.strict |
| InvokeModel (Anthropic Claude) | tools[].strict |
| InvokeModel (Open-weight models) | tools[].function.strict |
They're slightly different. It's important to be aware of these differences when using different models.
For more details, please refer to the official documentation.
First request may be slower
I noticed that the first request with Structured Outputs might feel a bit slower, likely due to internal schema validation processing. However, if you reuse the same schema, it appears to be cached for 24 hours, making subsequent requests feel faster than the first one.
Conclusion
It's great that structured output can now be easily achieved with Bedrock APIs!!
Let's make use of this feature when we want to force inference results into a specific format!
I hope this article was helpful. Thanks for reading to the end!
