AI APIをWebアプリに組み込むと、AIの返答をそのまま画面に出すだけでなく、アプリ側で扱えるデータとして受け取りたくなります。問い合わせ分類、レビュー判定、記事の構成案、フォーム入力の整理、管理画面の下書き生成などでは、「文章」よりも「決まった形のJSON」が必要です。
ただ、AIに「JSONで返して」と頼むだけでは、キー名が変わる、説明文が混ざる、配列のはずが文字列になる、といった揺れが残ります。軽い試作ならそれでもよい場合がありますが、本番の処理でDB保存、画面表示、条件分岐に使うなら、出力形式をもう一段しっかり固定したくなります。
そこで使うのがStructured Outputsです。Structured Outputsを使うと、AIの出力をJSON Schemaに沿った形へ近づけられます。この記事では、OpenAI APIのStructured Outputsの使い方を、JavaScript実装者向けに「何を固定するのか」「どうスキーマを書くのか」「どこでエラーを見るのか」まで整理します。
JSONそのものの基本や、プロンプト指定、JSON modeとの違いから確認したい場合は、先に AIにJSONを出力させる方法|プロンプト指定とスキーマ指定の違い を読むとつながりやすいです。この記事では、そこから一歩進んで、JSON Schemaで出力形式を固定する部分に集中します。
この記事の内容は、2026年7月9日時点のOpenAI API公式ドキュメントをもとにしています。APIのパラメータ名、モデル名、SDKヘルパー、対応スキーマは変わることがあるため、実装時には必ず公式ドキュメントも確認してください。
結論:Structured OutputsはAIの返答をJSON Schemaに合わせたいときに使う
Structured Outputsは、AIに自由な文章を書かせるための機能ではなく、アプリ側が期待する構造に合わせて返答を受け取るための機能です。
たとえば、次のような結果が毎回ほしいとします。
{
"category": "billing",
"priority": "high",
"summary": "請求金額が想定より高い理由の確認依頼",
"needs_human_review": true
}
この形が安定していれば、アプリ側では次の処理がしやすくなります。
categoryに応じて担当チームへ振り分けるpriorityがhighの問い合わせを上に表示するsummaryを一覧画面に出すneeds_human_reviewがtrueのものだけ確認キューへ送る- DBへ保存する前に型を確認する
OpenAI公式ドキュメントでは、JSON modeは有効なJSONを出すための機能、Structured OutputsはJSON Schemaへの準拠を扱う機能として説明されています。つまり、JSONとして壊れていないことよりも、「期待したキーと型で返ってくること」が重要な場合にStructured Outputsを選びます。
整理すると、役割は次のように分けられます。
| 方法 | 主な目的 | 向いている場面 |
|---|---|---|
| プロンプトでJSON指定 | JSON風の形で返すように頼む | 試作、手動確認、軽い下書き |
| JSON mode | 有効なJSONを返しやすくする | 単純な構造化、後段で検証する処理 |
| Structured Outputs | JSON Schemaに沿った出力を扱う | 本番のデータ変換、UI表示、DB保存、判定処理 |
| Function Calling | AIに関数やツールを選ばせる | 外部API実行、DB検索、処理の分岐 |
この記事では、Function Callingそのものは深掘りしません。AIが外部関数を選んで実行する流れは、後続のFunction Calling記事に分けます。ここでは「AIの返答を固定されたJSONにする」ことだけを扱います。
Structured Outputsで作るもの
この記事では、問い合わせ文を入力し、AIに分類結果を返してもらう例で考えます。
入力は次のような文章です。
先月より請求金額が高い理由を確認したいです。急ぎで見てほしいです。
出力としてほしいJSONは次の形です。
{
"category": "billing",
"priority": "high",
"summary": "請求金額が高い理由を急ぎで確認したい問い合わせ",
"needs_human_review": true,
"confidence": 0.86
}
この例では、各フィールドに役割を持たせます。
| キー | 型 | 役割 |
|---|---|---|
category |
文字列 | 問い合わせカテゴリ |
priority |
文字列 | 対応優先度 |
summary |
文字列 | 一覧表示用の短い要約 |
needs_human_review |
真偽値 | 人間確認が必要か |
confidence |
数値 | 分類の自信度 |
このように、Structured Outputsでは先にアプリ側で欲しいデータ構造を決めます。AIに自由に考えさせるのではなく、「この箱に入る形で結果を返してもらう」と考えると設計しやすいです。
APIレスポンスとして受け取ったJSONをJavaScriptで扱う基本は、JavaScriptでJSONデータを取得する方法 や Reactで取得したJSONデータを画面に表示する方法 ともつながります。
前提環境を確認する
この記事のコード例は、考え方を示すための最小例です。実際のSDKバージョン、モデル名、レスポンス形式は、利用時点の公式ドキュメントに合わせてください。
前提は次のとおりです。
- サーバー側のJavaScriptからOpenAI APIを呼び出す
- APIキーは環境変数などで管理する
- ユーザー入力はそのまま信用しない
- AIの出力はアプリ側でも確認してから保存する
- Function Callingではなく、返答そのものをJSON Schemaで固定する
ブラウザから直接AI APIを呼ぶと、APIキー漏洩や利用量制御の問題が起きます。AI機能は基本的にサーバー側を経由させます。クライアントとサーバーの役割は、クライアントサーバーシステムとは も参考になります。
また、Structured Outputsを使っても、業務判断そのものを完全にAI任せにしてよいわけではありません。個人情報、契約、医療、金融、法務などの判断に使う場合は、人間確認やログ、再試行、権限管理を組み合わせます。AI利用の安全面は、生成AIの安全な使い方|情報漏洩・著作権・間違いを防ぐチェックリスト もあわせて確認してください。
まずJSON Schemaを設計する
Structured Outputsでは、最初にJSON Schemaを設計します。今回の問い合わせ分類なら、次のようなスキーマになります。
{
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "login", "bug", "cancellation", "other"],
"description": "問い合わせのカテゴリ"
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "対応優先度"
},
"summary": {
"type": "string",
"description": "問い合わせ内容の短い要約"
},
"needs_human_review": {
"type": "boolean",
"description": "人間の確認が必要かどうか"
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1,
"description": "分類結果の自信度"
}
},
"required": [
"category",
"priority",
"summary",
"needs_human_review",
"confidence"
],
"additionalProperties": false
}
ポイントは次の3つです。
- キー名をアプリ側の処理に合わせて明確にする
enumで選択肢を絞れるものは絞るadditionalProperties: falseで余計なキーを許可しない
OpenAI公式ドキュメントでは、Structured Outputsで使えるJSON Schemaはサブセットとして案内されています。文字列、数値、真偽値、整数、オブジェクト、配列、enum、anyOfなどは扱えますが、すべてのJSON Schema機能がそのまま使えるわけではありません。
また、Structured Outputsでは、すべてのフィールドを required に含める必要があります。任意項目のように扱いたい場合は、キー自体を省略させるのではなく、type に ["string", "null"] のような形を使い、値として null を許可する設計にします。
そのため、最初から複雑なスキーマにしすぎるより、アプリ側が本当に必要とするキーだけに絞るのが現実的です。スキーマが大きいほど、実装者もレビューしにくくなり、AIの出力が失敗したときの原因も追いにくくなります。
最小コードを書く
次に、Responses APIでStructured Outputsを使う最小例を見てみます。
import OpenAI from "openai";
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
const inquirySchema = {
type: "object",
properties: {
category: {
type: "string",
enum: ["billing", "login", "bug", "cancellation", "other"],
description: "問い合わせのカテゴリ",
},
priority: {
type: "string",
enum: ["low", "medium", "high"],
description: "対応優先度",
},
summary: {
type: "string",
description: "問い合わせ内容の短い要約",
},
needs_human_review: {
type: "boolean",
description: "人間の確認が必要かどうか",
},
confidence: {
type: "number",
minimum: 0,
maximum: 1,
description: "分類結果の自信度",
},
},
required: [
"category",
"priority",
"summary",
"needs_human_review",
"confidence",
],
additionalProperties: false,
};
const response = await openai.responses.create({
model: "gpt-5.5",
input: [
{
role: "system",
content:
"問い合わせ文を分類し、指定されたスキーマに合うJSONだけを返してください。",
},
{
role: "user",
content:
"先月より請求金額が高い理由を確認したいです。急ぎで見てほしいです。",
},
],
text: {
format: {
type: "json_schema",
name: "inquiry_classification",
strict: true,
schema: inquirySchema,
},
},
});
if (response.status === "incomplete") {
throw new Error(
`Response incomplete: ${response.incomplete_details?.reason}`
);
}
const message = response.output.find((item) => item.type === "message");
const content = message?.content?.[0];
if (!content) {
throw new Error("No response content");
}
if (content.type === "refusal") {
throw new Error(`Model refused: ${content.refusal}`);
}
if (content.type !== "output_text") {
throw new Error(`Unexpected content type: ${content.type}`);
}
const result = JSON.parse(content.text);
console.log(result.category);
console.log(result.priority);
console.log(result.summary);
このコードでは、text.format.type に json_schema を指定し、strict: true と schema を渡しています。OpenAI公式ドキュメントでは、Responses APIでStructured Outputsを使う場合、この形式が案内されています。
モデル名は、公式ドキュメント上の例に合わせて gpt-5.5 としています。ただし、利用できるモデルや推奨モデルは変わるため、実装時点の公式ドキュメントとアカウントの利用可能モデルを確認してください。
処理の流れを分解する
Structured Outputsの処理は、次の順番で考えると理解しやすいです。
- アプリ側でほしいデータ構造を決める
- JSON Schemaとしてキー、型、必須項目、選択肢を書く
- API呼び出し時に
text.formatへスキーマを渡す - AIの返答を受け取る
- 拒否、未完了、想定外のレスポンスを確認する
- JSONとしてパースする
- アプリ側の型やビジネスルールでも確認する
重要なのは、Structured Outputsがあるからといって、アプリ側の確認処理が不要になるわけではない点です。AIの出力がスキーマに沿っていても、値が業務上妥当とは限りません。
たとえば confidence が 0.99 でも、本当に正しい分類とは限りません。priority が high でも、自動で顧客対応を完了させてよいとは限りません。Structured Outputsは「形を固定する機能」であり、「判断の正しさを保証する機能」ではありません。
実務では、次のような後処理を入れます。
confidenceが低ければ人間確認に回す- 特定カテゴリは必ず担当者承認を挟む
- 保存前にサーバー側のスキーマバリデーションを通す
- エラー時はリトライか通常フォーム処理へフォールバックする
- 入力と出力を監査ログとして残す
Zodを使ってスキーマの二重管理を減らす
JavaScriptやTypeScriptで実装する場合、JSON Schemaとアプリ側の型定義が別々になると、あとからズレやすくなります。
たとえば、JSON Schemaでは needs_human_review が必須なのに、TypeScript側では任意プロパティとして扱っていると、実行時のバグにつながります。逆にTypeScript側だけ修正してJSON Schemaを更新し忘れることもあります。
OpenAI公式ドキュメントでは、ZodなどのSDKヘルパーを使って、スキーマと型のズレを減らす方法も案内されています。考え方としては、次のようにZodで構造を定義し、それをStructured Outputsに渡します。
import OpenAI from "openai";
import { zodTextFormat } from "openai/helpers/zod";
import { z } from "zod";
const InquiryClassification = z.object({
category: z.enum(["billing", "login", "bug", "cancellation", "other"]),
priority: z.enum(["low", "medium", "high"]),
summary: z.string(),
needs_human_review: z.boolean(),
confidence: z.number(),
});
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
const response = await openai.responses.parse({
model: "gpt-5.5",
input: [
{
role: "system",
content: "問い合わせ文を分類してください。",
},
{
role: "user",
content: "ログインできず、請求画面も確認できません。",
},
],
text: {
format: zodTextFormat(
InquiryClassification,
"inquiry_classification"
),
},
});
const result = response.output_parsed;
この書き方にすると、JavaScript側で扱う型と、AIに求める出力構造を近づけやすくなります。実際にどこまで型推論やバリデーションを使うかは、利用しているSDKやビルド環境に合わせて確認してください。
小さな記事サンプルでは手書きのJSON Schemaでも十分ですが、実務ではZodやPydanticのような型定義からスキーマを作るほうが運用しやすい場面が多いです。
よくあるエラーと原因
Structured Outputsでつまずきやすいポイントを整理します。
| 症状 | 原因 | 見直す場所 |
|---|---|---|
| APIがスキーマエラーになる | 対応していないJSON Schema機能を使っている | schema、anyOf、型別キーワード |
| 余計なキーを許可してしまう | additionalProperties: false がない |
各object定義 |
| 必須項目が抜ける想定になっている | required の設計が曖昧 |
必須項目とnull許可 |
| 出力が途中で切れる | 出力トークン上限に達している | max_output_tokens、スキーマの大きさ |
| 拒否レスポンスが返る | 入力内容や依頼内容が安全上応答できない | refusalの処理 |
| 分類結果は形だけ正しいが内容が変 | 入力がタスクに合っていない、指示が曖昧 | system指示、入力チェック、例示 |
OpenAI公式ドキュメントでは、Structured Outputsでも間違いが起きる可能性があるため、指示の調整、例示、タスク分割などで改善することが案内されています。スキーマは形を縛れますが、入力がタスクに合っていない場合、モデルが無理にスキーマへ合わせようとして不自然な結果を返すことがあります。
そのため、実務では「スキーマに合っているか」と「業務上使ってよいか」を別々に見ます。
function shouldSendToHuman(result) {
if (result.needs_human_review) return true;
if (result.confidence < 0.7) return true;
if (result.category === "billing" && result.priority === "high") {
return true;
}
return false;
}
このように、Structured Outputsの結果をそのまま信じるのではなく、アプリ側のルールで最後の判断を挟むと安全です。
スキーマ設計のコツ
Structured Outputsを実務で使うときは、スキーマの書き方が品質に直結します。
まず、キー名は人間にもAIにもわかりやすい名前にします。cat や flag1 のような省略名より、category や needs_human_review のように意味が伝わる名前のほうが読みやすく、保守もしやすいです。
次に、自由文字列にしなくてよい項目は enum にします。カテゴリ、優先度、ステータス、表示タイプなどは選択肢を固定しやすい項目です。
一方で、要約文や説明文まで厳しくしすぎると、運用しにくくなることがあります。たとえば「50文字以内」といった条件は、スキーマだけでなく、プロンプトや後段のチェックで補うほうが扱いやすい場合があります。
設計時は、次の質問を順番に確認すると整理しやすいです。
- このキーは画面表示に使うのか、DB保存に使うのか
- この値は自由入力でよいのか、選択肢にできるのか
- 値が空でもよいのか、必ず必要なのか
- AIが間違えたときに、どの処理へ逃がすのか
- あとで集計したい値か、単なる表示文か
最初から万能スキーマを作ろうとすると、記事構成、問い合わせ分類、レビュー判定、UI生成などを全部混ぜたくなります。しかし、それをすると重くなります。1つのStructured Outputsは、1つの明確なタスクに絞るほうが安定します。
JSON modeとの使い分け
JSON modeとStructured Outputsは似ていますが、役割が違います。
JSON modeは、有効なJSONを返しやすくする機能です。たとえば「配列でタイトル案を返してほしい」「簡単な設定オブジェクトを返してほしい」といった場面では、JSON modeとアプリ側の検証で十分なことがあります。
一方、Structured Outputsは、JSON Schemaに沿った形で返答を受け取りたいときに使います。キー名、型、必須項目、enumなどを決め、アプリ側の処理とつなぐ前提の設計です。
目安は次のとおりです。
- 手動で確認する下書きなら、プロンプト指定でもよい
- JSONとしてパースしたいだけなら、JSON modeを検討する
- アプリ処理に直結するなら、Structured Outputsを検討する
- 外部APIやDB処理をAIに選ばせるなら、Function Callingを検討する
JSON modeの基本とパース処理は、前の記事 AIにJSONを出力させる方法 で扱っています。この記事では、その次の段階として、スキーマ固定に集中しています。
Function Callingとの違い
Structured Outputsには、返答の形式を固定する使い方と、Function Callingの引数を構造化する使い方があります。ここが少し混乱しやすいところです。
ざっくり分けると、次のようになります。
| 使い方 | 目的 | 例 |
|---|---|---|
| Structured Outputs | AIの返答を決まったJSONにする | 問い合わせ分類、構成案、レビュー結果 |
| Function Calling | AIに呼び出す関数と引数を選ばせる | 天気API、注文検索、在庫確認 |
たとえば、問い合わせを分類して管理画面に表示するだけなら、Structured Outputsで足ります。分類結果を見て「請求システムのAPIを呼ぶ」「ユーザー情報をDBから検索する」といった処理までAIに選ばせたいなら、Function Callingの領域になります。
この記事では、外部関数の呼び出しは扱わず、AIからアプリへ返るデータの形を固定することに絞っています。次に進むなら、Function Callingを学ぶと、AIアプリが外部システムと連携できるようになります。
セキュリティ・料金・運用の注意点
Structured Outputsを使うと、AIの返答がアプリに組み込みやすくなります。そのぶん、運用面の設計も大事になります。
まず、APIキーは必ずサーバー側で管理します。フロントエンドに埋め込むと、第三者に使われるリスクがあります。APIキーや環境変数の扱いは、AI API実装では早めに固めるべき部分です。
次に、ユーザー入力はそのまま信用しません。問い合わせ文、レビュー文、フォーム入力などには、想定外の内容や攻撃的な内容が含まれることがあります。Structured Outputsに渡す前に、文字数制限、禁止内容チェック、ログ設計を入れます。
また、スキーマが大きいほど入力トークンが増えます。説明文や選択肢を増やしすぎると、料金や遅延にも影響します。最初は小さなスキーマから始め、必要になった項目だけ増やすのが扱いやすいです。
運用では、次の項目を最低限見ておくと安心です。
- 入力文字数の上限
- 出力トークン上限
- refusalやincompleteの扱い
- リトライ回数とフォールバック
- ログに保存する内容と保存しない内容
- 人間確認に回す条件
AI APIは便利ですが、失敗時の動きまで決めておかないと、管理画面や自動処理の中で原因が追いにくくなります。Structured Outputsは、失敗時の切り分けをしやすくするためにも役立ちます。
実務で使うときの実装パターン
最後に、実務向けの実装パターンをまとめます。
1つ目は、サーバー側にAI分類用の小さな関数を作る方法です。UIやルーティングから直接APIを呼ぶのではなく、classifyInquiry() のような関数に閉じ込めます。
async function classifyInquiry(text) {
if (!text || text.length > 2000) {
throw new Error("Invalid inquiry text");
}
const response = await openai.responses.create({
model: "gpt-5.5",
input: [
{
role: "system",
content: "問い合わせ文を分類してください。",
},
{
role: "user",
content: text,
},
],
text: {
format: {
type: "json_schema",
name: "inquiry_classification",
strict: true,
schema: inquirySchema,
},
},
});
if (response.status === "incomplete") {
return {
ok: false,
reason: response.incomplete_details?.reason ?? "incomplete",
data: null,
};
}
const message = response.output.find((item) => item.type === "message");
const content = message?.content?.[0];
if (content?.type === "refusal") {
return {
ok: false,
reason: "refusal",
data: null,
};
}
if (content?.type !== "output_text") {
return {
ok: false,
reason: "unexpected_response",
data: null,
};
}
return {
ok: true,
reason: null,
data: JSON.parse(content.text),
};
}
2つ目は、AIの結果をすぐDBへ保存せず、確認ステップを挟む方法です。特に問い合わせ分類やレビュー判定では、AIの結果を「提案」として扱い、担当者が確認してから確定する設計にすると安全です。
3つ目は、スキーマを機能ごとに分ける方法です。問い合わせ分類、記事構成案、商品レビュー要約、フォーム補完を1つの巨大スキーマで処理しようとせず、それぞれ別のスキーマにします。機能ごとにスキーマを分けると、改善やテストもしやすくなります。
まとめ
Structured Outputsは、AIの出力をJSON Schemaに沿った形で扱いたいときに使う機能です。JSON modeが「有効なJSONを出しやすくする」ための機能なら、Structured Outputsは「アプリ側が期待する構造に合わせる」ための機能です。
実装の流れは、次のとおりです。
- アプリ側で必要なデータ構造を決める
- JSON Schemaとしてキー、型、必須項目、enumを定義する
text.formatにjson_schemaとstrict: trueを指定する- refusal、incomplete、想定外レスポンスを処理する
- スキーマに沿っていても、業務上の確認はアプリ側で行う
大事なのは、Structured Outputsを「AIを完全に正しくする機能」と考えないことです。これは、AIの返答をアプリで扱いやすい形にそろえるための機能です。正しさの確認、権限管理、ログ、人間確認は、アプリ側の設計として別に用意します。
次に進むなら、Function Callingを学ぶとよいです。Structured Outputsで返答形式を固定し、Function Callingで外部APIやアプリ内関数を選ばせる流れを理解すると、AIアプリの設計範囲が一気に広がります。