ツール活用

生成AIに正しいJSONを出力させる方法

生成AIのJSONが壊れる原因を整理し、項目と型の定義、構造化出力、受信側の検証、再試行までを安全な実装例で解説します。

  • #生成AI
  • #JSON
  • #構造化出力
  • #JSON Schema

生成AIへ「JSONで答えて」と頼んだのに、説明文やMarkdownのコードブロックが混ざる、項目名が毎回変わる、数値が文字列になる。このような出力をそのままプログラムへ渡すと、読み込みエラーだけでなく、誤った値で後続処理が進む原因になります。

大切なのは、プロンプトを工夫するだけで解決しようとしないことです。必要なデータの契約をJSON Schemaで表し、対応する構造化出力機能を使い、最後は受信側でも検証します。失敗時の再試行には上限を設け、注文、送信、削除などの操作はJSONが正しいだけで自動実行しない設計が必要です。

この記事では、会議メモから作業項目を取り出す例を使い、生成から検証までを順番に組み立てます。JSON自体の標準仕様とJSON Schemaの公式資料、構造化出力を提供するAPIの公式説明は、2026年7月23日と24日に確認しました。

1. JSON出力が壊れる原因

JSONは、機械が構造化データを交換するためのテキスト形式です。IETFのRFC 8259では、文字列、数値、真偽値、null、オブジェクト、配列を表せます。人には似て見えても、次の出力は正しいJSONではありません。

```json
{
  "title": "資料を確認",
  "done": False,
}
```

問題は三つあります。コードブロックを示す記号が本文に含まれ、真偽値がJSONで定められた小文字のfalseではなく、最後の項目の後ろに余分なカンマがあります。ほかにも、一重引用符、エスケープされていない改行、途中で切れた配列などが構文エラーになります。

構文が正しくても、アプリにとって正しいとは限りません。"priority": "3"は有効なJSONですが、受信側が整数を想定していれば型違反です。必要なtitleが無い、許可していないmemoが増える、日付欄に「来週くらい」が入る、といった意味上の不一致もあります。

つまり、確認は少なくとも二段階です。

  1. JSONとして読み込めるか。
  2. 必要な項目、型、値の範囲に合うか。

自然言語モデルは文章を生成する仕組みなので、単に「JSONで」と書くだけでは、親切な前置きや補足を足す場合があります。長い出力が上限で途中終了することもあります。まずは「見た目がJSON」と「システムが受理できるデータ」を分けて考えます。

2. 必要な項目と型を定義する

最初に出力例を書くのではなく、受信側が本当に使う項目を決めます。会議メモから作業項目を抽出するなら、次のように整理できます。

項目必須制約
title文字列はい1〜100文字
assignee文字列またはnullはい不明なら推測せずnull
due_date文字列またはnullはい分かる場合だけYYYY-MM-DD
priority文字列はいlowmediumhighのいずれか

JSON Schemaでは、typeで型、propertiesで項目ごとの規則、requiredで必須項目を表せます。propertiesに書いただけでは必須にならないため、欠けて困る項目はrequiredにも列挙します。余計な項目を受け取りたくない場合はadditionalProperties: falseを指定します。

{
  "type": "object",
  "properties": {
    "tasks": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "title": { "type": "string", "minLength": 1, "maxLength": 100 },
          "assignee": { "type": ["string", "null"] },
          "due_date": { "type": ["string", "null"] },
          "priority": {
            "type": "string",
            "enum": ["low", "medium", "high"]
          }
        },
        "required": ["title", "assignee", "due_date", "priority"],
        "additionalProperties": false
      }
    }
  },
  "required": ["tasks"],
  "additionalProperties": false
}

日付のformatに対応する検証器もありますが、JSON Schema Draft 2020-12の仕様では、formatを注釈として扱う実装と、妥当性検査に使う実装があります。そのため、利用中の検証ライブラリの挙動を確認し、業務上重要な日付は受信側で形式と実在日を別に検査するのが安全です。

項目を増やすほど情報量は増えますが、不明な値をAIが埋める余地も増えます。使わない項目は削り、「不明ならnull」「候補は列挙した値だけ」のように、欠損の表し方も契約に含めます。

3. 出力例と禁止事項を渡す

構造化出力機能を使えない環境では、スキーマに加えて、短い正例と禁止事項をプロンプトへ渡します。依頼と出力規則を混ぜず、役割を分けて書くと確認しやすくなります。

目的:
会議メモから、明示された作業項目だけを抽出する。

出力規則:
- JSONオブジェクトだけを返す。
- 説明文とMarkdownのコードブロックを付けない。
- tasksは配列にする。
- 担当者または期限が不明なら推測せずnullにする。
- priorityはlow、medium、highのいずれかにする。
- 入力文に書かれていない作業を追加しない。

出力例:
{"tasks":[{"title":"見積書を確認する","assignee":"佐藤","due_date":null,"priority":"medium"}]}

例は「形」を教えるためのもので、実データと誤認されない架空の内容にします。反対に、空配列、null、日本語の引用符、本文中の改行など、壊れやすい境界条件もテスト入力に含めます。

禁止事項だけを長く並べても、構文上取り得る次の文字を制限するわけではありません。プロンプト方式は改善策にはなりますが、スキーマへの適合を保証する仕組みではないため、後段の解析と検証は省略できません。AIへの依頼条件の整理方法は、AIに作業を頼む「依頼書」の作り方も参考になります。

4. 構造化出力機能を使う

利用するAPIがJSON Schemaによる構造化出力に対応しているなら、文章で形式を頼むだけでなく、その機能へスキーマを渡します。OpenAIの公式説明では、JSON modeは有効なJSONの生成を助けますが、特定のスキーマへの適合までは保証しません。Structured Outputsでstrict: trueを使うと、対応範囲内のJSON Schemaへ出力を制約できます。

方法JSONとしての構文指定スキーマへの適合受信側で必要な確認向いている場面
プロンプトだけ保証されない保証されないJSON解析、スキーマ検証、業務ルール構造化出力に非対応の環境、試作
JSON mode有効なJSONを生成するための機能保証されないスキーマ検証、業務ルール、途中終了自由なJSONを受け取りたい場合
Structured Outputs有効なJSON対応範囲内で制約できる拒否・途中終了、業務ルール、元資料との照合項目と型が決まったアプリ連携

この表の「適合」は形式上の話です。Structured Outputsを使っても、入力に無い事実の生成や、古い情報、業務上許されない値まで正しくなるわけではありません。

ただし、「スキーマに合う」と「内容が正しい」は別です。due_dateが文字列という条件を満たしても、元の会議メモに無い期限をAIが作れば内容は誤りです。また、APIが安全上の理由で回答を拒否した場合、出力上限などで生成が途中終了した場合は、通常の成功JSONとは別に扱う必要があります。

実装前には次を公式資料で確認します。

  • 利用モデルが構造化出力に対応しているか。
  • JSON Schemaのどの機能がサポート対象か。
  • 拒否、途中終了、通信エラーをどう判定するか。
  • スキーマを変更した最初の呼び出しで遅延が増える可能性があるか。

製品名、対応モデル、APIの書式は更新されるため、古いサンプルをそのまま固定しないでください。本稿では考え方を中心にし、特定モデル名や料金を実装条件にしていません。

構造化出力は、アプリへ返す最終データと、外部ツールを呼ぶ引数の両方で使われます。後者は実際の操作につながるため、引数がスキーマに合っただけで実行せず、権限、対象ID、金額、件数などを別のルールでも検査します。

5. 受信側で検証する

AIの返答は、信頼済みの内部データではなく外部入力として扱います。構造化出力を使っていても、通信の中断、API仕様の変更、モデルの拒否、アプリ側の取り違えに備えて受信側で検証します。

処理順は次の通りです。

  1. HTTPステータスやSDKのエラーを確認する。
  2. 拒否や途中終了でないことを確認する。
  3. 返答をJSONとして解析する。
  4. 同じJSON Schemaで検証する。
  5. 業務ルールを検証する。
  6. 合格したデータだけを保存または次工程へ渡す。

業務ルールとは、スキーマだけでは判断しにくい条件です。たとえば、期限が会議日より前でないか、担当者が登録済みユーザーか、同じ作業が重複していないかを確認します。金額なら通貨と上限、予約なら時刻と空き枠、ファイル操作なら対象パスの許可範囲が必要です。

検証エラーは、tasks[0].due_dateの形式が不正のように、場所と理由を記録します。ただし、会議本文や個人情報をログへ丸ごと残さないようにします。AIの出力と検証結果を分離して記録すれば、モデルを変更したときの比較にも使えます。

JSON Schemaの検証だけで、文章の事実性までは確かめられません。引用、固有名詞、数値を扱う場合は、元資料との照合が別途必要です。AIのハルシネーションが起きる理由と対策では、形式検査の外側に残る内容上の誤りを確認する手順を解説しています。

6. 失敗時だけ再試行する

再試行は、すべての呼び出しに機械的に行うのではなく、直せる失敗に限定します。通信の一時障害、途中終了、JSON解析エラー、スキーマ違反は再試行候補です。一方、入力自体に必要情報が無い、APIが安全上の理由で拒否した、権限が無い、といった問題は同じ依頼を繰り返しても解決しません。

再試行時は、前回の出力全文をそのまま追加するのではなく、検証器が返した最小限のエラーを伝えます。

前回の出力は次の検証に失敗しました。
- tasks[0].priorityはlow、medium、highのいずれかが必要です。

元の会議メモから再生成し、同じJSON Schemaに合うJSONだけを返してください。

回数は2回など小さな上限を決め、指数バックオフなどAPI提供者が推奨する待ち方に従います。上限に達したら、空の成功データへ置き換えず、処理を失敗として止めます。保存済みデータを更新する処理では、同じ再試行で二重登録が起きないよう、呼び出しごとの識別子や重複防止も必要です。

検証に落ちた出力だけを集めると、スキーマや指示の弱点が見えます。同じ項目で失敗が続くなら再試行回数を増やすより、型、enum、不明値の表し方、入力文の分割方法を見直します。検査を継続的な門番にする考え方は、AIが書く記事に「機械の門番」を付ける方法にも共通します。

7. 安全な実装例

次は、特定のSDKに依存しないJavaScript風の疑似コードです。callModelWithSchemaは利用中の公式SDK、validateは選んだJSON Schema検証器へ置き換えます。

const MAX_ATTEMPTS = 2;

async function extractTasks(meetingNote) {
  if (typeof meetingNote !== "string" || meetingNote.length > 20000) {
    throw new Error("入力の形式または長さが不正です");
  }

  let lastError;

  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
    const response = await callModelWithSchema({
      input: meetingNote,
      schema: taskSchema,
      strict: true
    });

    if (response.refused) {
      throw new Error("モデルが処理を拒否しました");
    }
    if (response.status !== "completed") {
      lastError = new Error("生成が完了していません");
      continue;
    }

    const result = validate(taskSchema, response.output);
    if (!result.valid) {
      lastError = new Error(summarizeErrors(result.errors));
      continue;
    }

    const safeTasks = response.output.tasks.filter(task =>
      knownUsers.has(task.assignee) || task.assignee === null
    );

    return { tasks: safeTasks };
  }

  throw lastError ?? new Error("構造化出力に失敗しました");
}

この例の要点は、入力の上限、拒否と途中終了の分岐、スキーマ検証、業務ルール、再試行上限を一か所で確認していることです。実運用ではタイムアウト、レート制限、個人情報を除いた監査ログも加えます。抽出結果をメール送信、発注、削除などへつなぐ場合は、その直前に人の承認や別の確定的な検査を置きます。

導入時は、正常例だけでなく、空のメモ、極端に長い入力、指示を装った本文、担当者不明、存在しない日付、重複項目、途中終了を試します。AIの出力を信用するのではなく、「不正な出力が来ても受信側が安全に止まるか」を合格基準にします。

8. よくある質問

「JSONで答えて」と書くだけでは不十分ですか

不十分な場合があります。説明文やコードブロックが混ざる可能性があり、構文が正しくても項目名や型が一定になるとは限りません。利用できるなら構造化出力を使い、受信側でも検証してください。

JSON modeとStructured Outputsの違いは何ですか

JSON modeは有効なJSONの生成を目的としますが、指定したスキーマへの適合までは保証しません。Structured Outputsは、APIが対応するJSON Schemaの範囲で、必須項目や型などへ出力を制約します。

additionalProperties: falseは必要ですか

想定外の項目を拒否したい場合に有効です。ただし、スキーマをallOfなどで組み合わせると意図しない不適合が起きることがあるため、利用する検証器と実際のスキーマでテストします。

スキーマに合格したら、そのまま保存や送信をしてよいですか

いいえ。スキーマ検証は形式の確認です。元資料に基づく内容か、対象IDや権限が正しいか、金額や件数が上限内かを別に確認し、外部送信や削除などは必要に応じて人の承認を挟みます。

検証に失敗したら何回まで再試行すべきですか

一律の正解はありませんが、無限再試行は避けます。まず1〜2回など小さな上限を置き、失敗率、待ち時間、費用を計測して調整します。情報不足や安全上の拒否は、同じ入力を繰り返さず人へ戻します。

関連記事

一次情報の確認記録