結論から:JSON.parse() の失敗は、たいてい「モデルが JSON を書けない」ことではない。JSON 値を一つだけ受け取るパーサに、チャット返信の全体を渡していることである。JSON.parse が受け取るのは JSON 文法(ECMA-262 / RFC 8259)の値、ちょうど一つ。Markdown フェンス、前後の説明、末尾カンマ、生の改行、打ち切り、JS / Python 方言は、いずれも即座に SyntaxError を投げる。2026 年の直し順はこうだ:Structured Output を使える、または Tool Calling の arguments を読めるなら、チャット本文をパースするな。パースせざるを得ないなら、先に抽出し、それから parse し、成功したら JSON Schema で検証する——最初から正規表現で「なんとかパースできるまで直す」な。
本稿は 2026年9月17日時点。本サイトにはすでに Structured Output とは何か、Prompt から Structured Output へ、OpenAI vs Gemini Structured Output、Gemini API で構造化 JSON を生成する、Tool Calling と JSON Schema がある。本稿が答えるのは次だけ:モデル出力がなぜ JSON.parse を通らないか、どの順で直すか。
JSON.parse が実際に受け取るもの
ブラウザと Node の JSON.parse が実装するのはJSON テキストであり、「それっぽい JavaScript オブジェクトリテラル」ではない。空白(スペース、タブ、改行、復帰)は値の両側に置ける。それ以外、入力はちょうど一つの値でなければならない:オブジェクト、配列、文字列、数値、true / false / null。値の後ろに非空白が来れば失敗する——Chrome はよく Unexpected non-whitespace character after JSON と書く。
これらは JS では動き、JSON では死ぬ。モデルは訓練データから日常的に写してくる:
| 書き方 | JS オブジェクト / JSON5 | JSON.parse |
|---|---|---|
| 末尾カンマ | {"ok": true,} 通る | 例外 |
| 単引用符 | {'ok': true} 通る | 例外 |
| コメント | // note 通る | 例外 |
| 裸のキー | {ok: true} 通る | 例外 |
undefined / NaN / Infinity | 言語にある | 例外 |
| 文字列内の生改行 | テンプレート文字列は許す | 例外;\n にせよ |
切り分けは一文:渡したのは「JSON 値一つ」か、「読みやすく包んだ段落」か。前者はパーサの仕事。後者は先に抽出せよ。
失敗原因の分類表
正確な文言と格闘する前に SyntaxError を分類せよ。Chrome、Safari、Node は同じバグを別の言い方で書く。クラスは少ない:
| 分類 | モデルがよく出すもの | 典型結果 | 先にすること |
|---|---|---|---|
| 包装 | ```json フェンス、「JSON はこちら」 | 先頭が { / [ ではない | フェンスを剥ぎ、バランスした値を切り出す |
| 方言 | 末尾カンマ、単引用符、コメント、裸のキー | Unexpected token | Structured Output に切り替え;JS としてパースするな |
| 壊れた文字列 | 未エスケープの "、生改行、全角カンマ | 文字列が早く終わる、またはキーの後ろに : がない | 列番号を見る;フィールド長に上限を付ける |
| 打ち切り | 閉じないオブジェクトや配列 | Unexpected end of JSON input | 出力上限を上げる;ストリーム終了を待つ |
| 複数値 | JSON が二つ、または最初の値の後ろに散文 | 最初の値の後ろに文字が残る | 最初の完全な値だけ切り出す |
| エンコード | BOM、ゼロ幅文字、二重 stringify | 奇妙なトークン、または parse が文字列を返す | BOM を除く;再パース前に typeof を見る |
Agent ではもう一条:Tool Calling の arguments は、すでにオブジェクトであるか、ベンダーが制約した JSON 文字列であることが多い。assistant メッセージ全体を JSON.parse に通すな。別チャネルである——Tool Calling が JSON Schema に依存する理由 を見よ。
フェンスと前後の説明
チャットモデルはコードをフェンスに入れるよう訓練されている。「JSON だけ」と書いても、返信はしばしばこうなる:
```json
{"ok": true, "id": "A-1024"}
```
Here is the result. I can explain the fields if you want.
先頭はバッククォートであり、{ ではない。JSON.parse は 0 列目で失敗する。先頭の「了解、JSON はこちら:」も、末尾の免責も同じバグ。より悪いのは値が二つ——サンプル、それから本物。塊全体をパースすれば、最初の } の直後で落ちる。
抽出は一条:最初のバランスした {} または [] を見つけ(文字列内の括弧は飛ばす)、そのスライスだけを JSON.parse に渡せ。フェンスは先に剥がせ。最初の { から最後の } まで欲張って切るな——文字列内の括弧や、説明に続く第二のオブジェクトで切る位置が狂う。
方言:末尾カンマ、単引用符、コメント、裸のキー
モデルは大量の JavaScript、Python、JSON5、YAML を見ている。「構造化データ」を求められると方言を混ぜる。以下はすべて JSON.parse では違法:
{
ok: true, // bare key + comment
'name': 'Ada', // single quotes
"tags": ["a",], // trailing comma
"flag": True // Python boolean
}
加えて undefined、NaN、Infinity、None。それぞれの言語では意味がある。JSON にあるのは null と有限の数だけ。JSON.parse を eval や new Function に差し替えて「受け入れる」と、パーサが任意コードの流入口になる。本番でやるな。
JSON5 と JSONC はコメントと末尾カンマを飲める。人が設定を手で書くならよい。モデル出力の既定パーサとしては弱い。文法を緩めた瞬間、「余分なカンマ」と「壊れた文字列」を区別できなくなる。緩い層が要るなら、抽出 + parse が失敗したあとに置き、修復後も Schema を通せ。
文字列と句読点:エスケープ、改行、全角とスマートクォート
合法な JSON 文字列は二重引用符で囲む。内側の " とバックスラッシュはエスケープ必須。制御文字は \n、\t、または \uXXXX。モデルがユーザーコメントを写すと、生の引用符と改行がフィールドに落ちる。文字列は早く終わり、次のカンマや CJK 文字が unexpected token になる。
CJK 出力には頻出の汚れがある:全角カンマ ,、全角コロン :、湾曲引用符 “” / ‘’。句読点に見えるが、コードポイントは 0x2C / 0x3A / 0x22 ではない。この「ほぼ JSON」は name の値の後ろで死ぬ:
{
"name": "Ada",
"ok": true
}
「半角の句読点を使え」をもう一文足しても直らない。長い文字列フィールドに maxLength を付け、モデルには句読点を打ち直させず原文を参照させ、最終チャネルは Structured Output にせよ。切り分けでは JSONバリデーター に貼り、ハイライトが何列で止まるかを見よ——全角カンマは一目で分かる。
打ち切りとストリーミング:Unexpected end of JSON input
Unexpected end of JSON input はほぼ常に、文法が終わる前にテキストが切れたことを意味する:} 欠け、] 欠け、閉じない文字列。2026 年のよくある源は三つ:出力 token 上限、安全フィルタの途中切断、未完了のストリーム chunk に JSON.parse を呼んだこと。
ストリーミング API が渡すのは差分である。最初の数 chunk は {"ok": tr かもしれない。それをパースすれば必ず失敗する。代わりにこうせよ:
- ストリーム終了(
finish_reason/stop)を待ってから、完全なバッファをパースする; - または token 単位で進む本物のストリーミング JSON パーサを使う——半端な値に
JSON.parseを呼ぶな; - 終了理由が
length/max_tokensなら、パースのバグではない。生成が終わっていない——上限を上げる、Schema を小さくする、または分割して出させよ。
打ち切り後に「} を自動補完」するのは下書き用の技だ。形はパースできても、フィールド欠けや文字列の途中切断が残る。修復したあとは必ず Schema 検証;失敗したら再試行。黙って保存するな。
不可視文字と二重エンコード
UTF-8 BOM(U+FEFF)は JSON の空白ではない。一部のコピー経路やゲートウェイが先頭に付ける;JSON.parse は 0 列目で unexpected token と報じる。ゼロ幅スペースとソフトハイフンも同じ。抽出前に replace(/^\uFEFF/, "")、続けて trim。
二重エンコードはより静かだ。一度 JSON.stringify すると文字列 "{\"ok\":true}" になる。外側の引用符付きのそれをパースすると、得られるのは文字列 {"ok":true} であり、オブジェクトではない。もう一度パースしてオブジェクトになる。一回で止めて .ok を読むと undefined——「パースは成功した」のにフィールドがない。再パースの前に typeof を見よ。「常に二回パース」と決め打ちするな;本物のオブジェクトなら例外になる。
直し順:先にチャネルを変え、次に抽出し、修復は最後
この順のほうが、プロンプトを積み増すより安定する:
- チャネルを変えよ。最終返答は Structured Output(OpenAI
response_format.json_schema、GeminiresponseMimeType+ Schema、Claudeoutput_config.format)。ツール引数は Tool Calling のargumentsを通せ。散文から掻き出すな。原理は Structured Output とは何か。 - 抽出。
```jsonフェンスを剥ぐ;最初のバランスした値を切り出す;BOM を落とす。 - 厳格に parse。
JSON.parseだけを使う。失敗したら原文とエラー位置を残せ。evalするな。 - Schema 検証。parse 成功は文法が合法だというだけ。欠けたフィールド、誤った型、余分なキーは JSON Schema / ajv が要る。Prompt から Structured Output へ を見よ。
- 修復は最後。
jsonrepairのような道具は括弧を閉じ、末尾カンマを落とせる。抽出 + parse が失敗したあと、かつ「直したテキストが意味を変え得る」と受け入れるときだけ使え。そのあとも 3 と 4 を通せ。修復器をグローバル既定パーサにするな。
プロンプトはなお有用だ:「フェンスなし、説明なし」。包装層の確率は下がる。しかし Schema の代替にはならず、JSON.parse を緩くもしない。2026 年にチャット本文を API として扱うと、フェンスと打ち切りで繰り返し払うことになる。
小さな抽出 + parse パイプライン
教材サイズの最小パイプライン:フェンスを剥ぎ、BOM を落とし、バランスした値を切り出し、それから JSON.parse。よくある包装は処理する。末尾カンマや全角句読点は直さない——それは Structured Output か、明示した修復層へ回せ。
function stripFence(text) {
const m = String(text).match(/```(?:json|JSON)?\s*([\s\S]*?)```/);
return m ? m[1] : String(text);
}
function sliceBalancedJson(text) {
const src = text.replace(/^\uFEFF/, "").trim();
const start = src.search(/[\{\[]/);
if (start < 0) throw new SyntaxError("No JSON value found");
const open = src[start];
const close = open === "{" ? "}" : "]";
let depth = 0, inStr = false, esc = false;
for (let i = start; i < src.length; i++) {
const ch = src[i];
if (inStr) {
if (esc) { esc = false; continue; }
if (ch === "\\") { esc = true; continue; }
if (ch === '"') inStr = false;
continue;
}
if (ch === '"') { inStr = true; continue; }
if (ch === open) depth++;
else if (ch === close) {
depth--;
if (depth === 0) return src.slice(start, i + 1);
}
}
throw new SyntaxError("Unterminated JSON value");
}
function parseModelJson(raw) {
return JSON.parse(sliceBalancedJson(stripFence(raw)));
}
スライサは「文字列の中にいるか」を追跡しなければならない。さもなくばフィールド値の { が早く閉じる。入れ子のオブジェクトと配列は depth で扱う。スライス成功後も parse が失敗するなら、失敗テキストをバリデータに貼り、上の分類表と照合せよ。この層に正規表現を積み増すな。
ローカルでエラー位置を見る
モデル出力をいきなり本番パーサへ送るな。ブラウザで三つ見よ:合法 JSON か;違法なら何列か;すでに Schema があるなら契約を満たすか。
- JSONバリデーター ——
SyntaxErrorの位置を見る;Schema があれば合わせて検証する。 - JSONフォーマッター ——整形できれば、たいていパースできる;失敗したら原文の全角カンマやフェンスを探す。
- JSON Diff ——parse 成功後、「モデルのオブジェクト」と「許す最小オブジェクト」を比べる。
データはブラウザを出ない。失敗したモデル返信、Schema、Tool Calling の arguments を並べて見るのに向く。フィールド名と required が安定してから、Host に配線せよ。
FAQ
「JSON に見える」のに JSON.parse が拒むのはなぜか?
人の目はフェンス、末尾カンマ、湾曲引用符、前後の説明を許す。JSON.parse は RFC 8259 の値をちょうど一つだけ受け取る。似ていることは、合法であることではない。
正規表現で ```json フェンスを消せば足りるか?
足りない。フェンスは包装の一層にすぎない。後ろに説明、第二の JSON、末尾カンマ、打ち切りが残る。フェンスを剥いだあとも、バランス切り出しと厳格な parse が要る。
JSON Mode と Structured Output の違いは何か?
JSON Mode はだいたい「JSON らしく見える」ことだけを縛り、フィールドと型は見ない。Structured Output はデコード時に JSON Schema で不正トークンを止める。プログラムが消費するなら Structured Output を優先せよ。JSON Mode だけ開いてチャット本文を JSON.parse するな。
jsonrepair や JSON5 を既定パーサにしてよいか?
いけない。失敗すべき入力を飲み、意味を変え得る。抽出 + JSON.parse が失敗したあとの修復層としてだけ使え。修ったあとも Schema 検証せよ。
ストリーム応答ではいつ JSON.parse を呼んでよいか?
ストリームが終わり、バッファが完全な値になってから。半端な chunk をパースすれば、毎回 Unexpected end of JSON input になる。到着と同時に消費するなら、ストリーミングパーサを使え。JSON.parse ではない。
パースは成功したがフィールドが違う。この記事の範囲か?
次の層である。JSON.parse が保証するのは文法だけ。欠けたフィールド、誤った型、余分なキーは Schema の問題——本サイトの Structured Output と Tool Calling 検証の二本を見よ。
まとめ
JSON.parse の失敗はチャネルの問題である。「JSON を出力せよ」をもう一文足しても直らない。チャットモデルはフェンスで包み、方言を混ぜ、token 上限で止まる。パーサが受け取るのはきれいな JSON 値、一つだけ。2026 年、モデルをプログラムへつなぐ正しい順は Structured Output / Tool Calling の arguments、次いで抽出 + 厳格な parse + Schema、最後が修復器である。
プロンプトは包装層を減らせる。文法は緩められない。失敗テキストをローカルのバリデータに貼り、何列で止まるかを見てから決めよ:フェンスを剥ぐか、チャネルを変えるか、出力上限を上げるか。モデルは変わってよい。JSON.parse が受け取るものと、フィールド契約は、変えてはならない。