GASでClaude APIの構造化出力で
壊れないJSONを受け取る方法
AIの回答をJSON.parseしたら失敗した——その原因の多くは、AIが余計な前置きや崩れたJSONを返すことです。Structured Outputs(構造化出力)を使えば、出力を必ず指定のJSON構造に従わせられます。GASからの実装を動くコードで解説します。
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は「この選択肢の中から選ばせる」指定です。分類の候補を固定でき、表記の揺れがなくなります。 各オブジェクトにrequiredとadditionalProperties: 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を使った業務自動化をご相談いただけます。構造化出力で崩れない仕組みまで含めて設計します。