GASでClaude APIの構造化出力で
壊れないJSONを受け取る方法

AIの回答をJSON.parseしたら失敗した——その原因の多くは、AIが余計な前置きや崩れたJSONを返すことです。Structured Outputs(構造化出力)を使えば、出力を必ず指定のJSON構造に従わせられます。GASからの実装を動くコードで解説します。

|対象: GAS / Claude API / 構造化出力

Table of Contents

なぜプロンプトだけのJSONは壊れるのか

GASからClaude API(AnthropicのAIをプログラムから呼び出す仕組み)を使い、回答をスプレッドシートに書き戻す——このとき多くの人がハマるのが、AIの回答をJSONとして解析する部分です。 プロンプトで「JSONで返してください」とお願いしても、AIは次のような崩し方をすることがあります。

前置きが付く

「はい、以下がJSONです:」と文章を足してから本体を返す

コードブロックで囲む

バッククォート3つで囲まれ、そのままではparseできない

末尾にコメント

JSONの後ろに「※緊急度は推定です」などを追記してしまう

キーの揺れ

「category」と頼んだのに「分類」や「type」で返ってくる

1件2件なら気付いて直せますが、数百行をループで処理すると、たった1件の崩れでスクリプト全体がSyntaxErrorで止まります。プロンプトの工夫やtry-catchでしのぐのが従来の方法でしたが、そもそも崩れさせない仕組みがStructured Outputsです。

Structured Outputsとは何か

Structured Outputs(構造化出力)は、AIの回答を「あらかじめ決めたJSONの形」に必ず従わせる機能です。 リクエストにoutput_config.formatというフィールドでスキーマ(JSONの設計図)を渡すと、Claudeは前置きもコードブロックも付けず、 スキーマ通りのJSON文字列だけを返します。プロンプトでの「お願い」と違い、出力形式が保証されるのが決定的な違いです。

ポイントは3つです。(1)SDK専用の機能ではないためGASのUrlFetchAppからも使えます。(2)追加のベータヘッダーは不要で、通常のリクエストにフィールドを1つ足すだけです。(3)受け取り側はcontent[0].textをJSON.parseするだけで、その解析が失敗しない前提でコードを書けます。

最小実装:スキーマを渡してJSONを受け取る

まずは問い合わせを「分類」と「緊急度」の2項目で受け取る最小例です。APIキーはPropertiesServiceに保管しておきます(コードへの直書きは避けます)。

const ENDPOINT = "https://api.anthropic.com/v1/messages";

function askStructured() {
  const apiKey = PropertiesService.getScriptProperties().getProperty("ANTHROPIC_API_KEY");

  const payload = {
    model: "claude-opus-4-8",
    max_tokens: 1024,
    // 出力を必ずこのJSON構造に従わせる
    output_config: {
      format: {
        type: "json_schema",
        schema: {
          type: "object",
          properties: {
            category: { type: "string", enum: ["見積依頼", "クレーム", "その他"] },
            urgency: { type: "integer" },
          },
          required: ["category", "urgency"],
          additionalProperties: false,
        },
      },
    },
    messages: [
      { role: "user", content: "次の問い合わせを分類して: 納期に間に合わないので至急連絡ください" },
    ],
  };

  const res = UrlFetchApp.fetch(ENDPOINT, {
    method: "post",
    contentType: "application/json",
    headers: {
      "x-api-key": apiKey,
      "anthropic-version": "2023-06-01",
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true,
  });

  const body = JSON.parse(res.getContentText());
  // content[0].text は必ずスキーマ通りのJSON文字列になる
  const data = JSON.parse(body.content[0].text);
  Logger.log(data.category); // 例: クレーム
  Logger.log(data.urgency);  // 例: 90
}

スキーマのenumは「この選択肢の中から選ばせる」指定です。分類の候補を固定でき、表記の揺れがなくなります。 各オブジェクトにrequiredadditionalProperties: falseを付けるのがルールです(後半で詳しく触れます)。

エラーと途中打ち切りに備えるガード

構造化出力でも、JSONが崩れる例外が2つあります。出力がトークン上限で途中打ち切りになるmax_tokensと、モデルが安全上の理由で応答を拒否するrefusalです。レスポンスのstop_reason(停止理由)を先に確認しておけば、安全に扱えます。

function parseStructured(res) {
  const body = JSON.parse(res.getContentText());

  // 1. APIエラー(401/400など)は content が無い
  if (body.type === "error") {
    throw new Error("APIエラー: " + body.error.message);
  }
  // 2. 出力が途中で切れると JSON が壊れる
  if (body.stop_reason === "max_tokens") {
    throw new Error("max_tokensに達しました。値を大きくしてください");
  }
  // 3. 安全上の拒否では構造が保証されない
  if (body.stop_reason === "refusal") {
    throw new Error("モデルが応答を拒否しました");
  }

  return JSON.parse(body.content[0].text); // ここは失敗しない前提で書ける
}

このガード関数を1つ用意しておくと、以降の呼び出しはparseStructured(res)を通すだけで済みます。max_tokensによる打ち切りは、出力の大きさに対して値が小さいときに起きるので、余裕を持たせるのが基本の対策です。

スプレッドシートを崩れず一括分類する

実務でありがちな「問い合わせシートを1行ずつ分類して書き戻す」処理です。 構造化出力なら、何百行を回してもJSONの崩れで止まりません。分類済みの行はスキップし、 前セクションのガード関数を再利用しています。

// スプレッドシートの問い合わせを崩れず一括分類する
function classifyInquiries() {
  const apiKey = PropertiesService.getScriptProperties().getProperty("ANTHROPIC_API_KEY");
  const sheet = SpreadsheetApp.getActive().getSheetByName("問い合わせ");
  const lastRow = sheet.getLastRow();
  if (lastRow < 2) return;

  // A列:本文 / B列:分類 / C列:理由
  const rows = sheet.getRange(2, 1, lastRow - 1, 3).getValues();

  const schema = {
    type: "object",
    properties: {
      category: { type: "string", enum: ["見積依頼", "サポート", "クレーム", "営業", "その他"] },
      reason: { type: "string" },
    },
    required: ["category", "reason"],
    additionalProperties: false,
  };

  rows.forEach((row, i) => {
    const text = row[0];
    if (!text || row[1]) return; // 空行・分類済みはスキップ

    const result = classifyOne(apiKey, schema, text);
    sheet.getRange(i + 2, 2).setValue(result.category);
    sheet.getRange(i + 2, 3).setValue(result.reason);
    Utilities.sleep(300); // レート制限対策に少し待つ
  });
}

function classifyOne(apiKey, schema, text) {
  const payload = {
    model: "claude-opus-4-8",
    max_tokens: 512,
    output_config: { format: { type: "json_schema", schema: schema } },
    messages: [{ role: "user", content: "次の問い合わせを分類してください:\n" + text }],
  };
  const res = UrlFetchApp.fetch("https://api.anthropic.com/v1/messages", {
    method: "post",
    contentType: "application/json",
    headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true,
  });
  return parseStructured(res); // 前セクションのガード関数を再利用
}

「0〜100のスコアで緊急度を付けたい」といった数値の範囲は、スキーマでは指定できません(次のセクション参照)。 その場合はcontentのプロンプト側で「緊急度は0〜100の整数で」と指示し、構造だけをスキーマで固定するのがコツです。

配列・ネストした構造を受け取る

1件のテキストから複数の項目を抜き出したいときは、type: "array"を使います。たとえば見積書の本文から明細行を全部取り出す、といったケースです。 配列の中の1件1件も、objectとして構造を固定できます。

// 1回の呼び出しで複数の明細を配列として受け取る
const schema = {
  type: "object",
  properties: {
    items: {
      type: "array",
      items: {
        type: "object",
        properties: {
          name: { type: "string" },
          price: { type: "integer" },
          taxable: { type: "boolean" },
        },
        required: ["name", "price", "taxable"],
        additionalProperties: false,
      },
    },
    total: { type: "integer" },
  },
  required: ["items", "total"],
  additionalProperties: false,
};

// 受け取り側は配列としてそのまま扱える
// const data = parseStructured(res);
// data.items.forEach(item => Logger.log(item.name + " / " + item.price));

受け取ったあとはdata.itemsが確実に配列になっているので、forEachでそのまま回せます。「配列のはずが1件だけ文字列で返ってきた」といった、後続処理を壊す不定形を気にせずに済みます。

スキーマの制約と対応モデル

1. 各オブジェクトに required と additionalProperties: false を付ける

これは構造化出力の必須ルールです。additionalProperties: falseは「決めたプロパティ以外は返させない」指定で、余計なキーの混入を防ぎます。付け忘れるとエラーになります。

2. 使えない制約は「プロンプト」で補う

minimum/maximum(数値の範囲)やminLength/maxLength(文字数)、再帰的なスキーマは使えません。範囲や文字数の条件は、プロンプトの文章で指示します。

3. 対応モデルと初回のわずかな遅延

構造化出力は最新世代のモデル(本記事のclaude-opus-4-8など)で使えます。新しいスキーマは初回だけ内部でコンパイルされ、その分わずかに時間がかかりますが、同じスキーマは一定時間キャッシュされるため2回目以降は速くなります。 なお、出典を付けるcitations機能とは同時に使えない点だけ注意します。

まとめ

AIの回答を安定してプログラムで扱う鍵は、プロンプトでお願いするのではなく、output_config.formatで構造を固定することです。GASからはUrlFetchAppにフィールドを1つ足すだけで導入でき、JSON.parseが失敗しない前提でループ処理を書けます。stop_reasonのガードと、スキーマで表せない条件をプロンプトで補う使い分けを押さえれば、 スプレッドシートの大量データもAIで安定して自動処理できます。

よくある質問

AIの回答を、あらかじめ決めたJSONの形(スキーマ)に必ず従わせる機能です。リクエストにoutput_config.formatでスキーマを渡すと、Claudeは「余計な前置き」や「壊れたJSON」を返さなくなり、GAS側のJSON.parseが失敗しなくなります。プロンプトで「JSONで返して」とお願いする方法と違い、構造が保証されるのが特長です。

プロンプトだけの場合、AIが「はい、以下がJSONです」といった前置きを付けたり、末尾にコメントを足したりして、JSON.parseが失敗することがあります。Structured Outputsはスキーマ側で出力形式を強制するため、こうした崩れが起きません。大量のデータをループで処理するときほど効果が大きくなります。

使えます。Structured OutputsはSDK専用の機能ではなく、リクエストのJSONにoutput_config.formatを1つ足すだけです。GASからはUrlFetchAppで通常どおりPOSTするだけで、追加のベータヘッダーも不要です。

各オブジェクトにはadditionalProperties: falseとrequiredの指定が必要です。一方で、minimum/maximum(数値の範囲)やminLength/maxLength(文字数)、再帰的なスキーマは使えません。「0〜100のスコア」のような範囲はスキーマではなくプロンプトで指示します。

基本的には保証されますが、stop_reasonがrefusal(安全上の拒否)やmax_tokens(出力の途中打ち切り)のときは、JSONが完成せず崩れることがあります。レスポンスのstop_reasonを確認し、これらのときはエラーとして扱うのが安全です。max_tokensは値を大きめにすると防げます。

AI×GASの業務自動化を
相談する。

Claude APIとGASを組み合わせた問い合わせ分類・データ抽出・帳票の自動生成など、AIを使った業務自動化をご相談いただけます。構造化出力で崩れない仕組みまで含めて設計します。

AI×GAS自動化を見る