JSON文字列のエスケープは手書き置換で組まない:JSON.stringifyに任せる理由を手を動かして確かめる

APIのリクエストボディやログを組み立てるとき、任意のテキストをJSON文字列の値として埋め込む場面は多い。このとき「ダブルクォートを \" に、改行を \n に置換すればいい」と手書きの replace を並べると、制御文字やサロゲートペアで簡単に壊れる。結論から言うと、自前で組まず JSON.stringify に任せるのが正解だ。手を動かして確かめる。

手書き置換が漏らすもの

素朴にやるとこうなりがちだ。

1
2
3
4
5
6
7
8
// アンチパターン:漏れが出る
function badEscape(s) {
return s.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\t/g, '\\t');
}
badEscape('タブ\tと改行\nと"引用"'); // 一見それらしい

だが JSON の仕様では、\n \t 以外の制御文字(U+0000〜U+001F)もすべてエスケープが必要だ。例えばバックスペース(U+0008)、フォームフィード(U+000C)、垂直タブ(U+000B)、キャリッジリターンなどが生テキストに混ざると、上の関数はそれらを素通しし、できあがった文字列は不正なJSONになる。

1
2
const s = 'ctrl' + String.fromCharCode(8, 12, 0) + 'end'; // BS/FF/NUL入り
JSON.parse('"' + badEscape(s) + '"'); // SyntaxError: 制御文字が生のまま

JSON.stringify に任せる

JSON.stringify は仕様どおり全制御文字をエスケープし、サロゲートペア(絵文字など)も正しく扱う。返り値は前後にダブルクォートが付くので、値部分だけ欲しいときは両端を1文字ずつ削る。

1
2
3
4
5
6
7
8
9
10
function jsonEscape(s) {
return JSON.stringify(s).slice(1, -1); // 前後の " を除去
}

jsonEscape('ctrl' + String.fromCharCode(8, 12) + 'end');
// "ctrl\b\fend" ← 制御文字が正しくエスケープされる

// 往復して元に戻ることを確認
const src = 'タブ\tと"引用"と絵文字\u{1F600}と' + String.fromCharCode(0);
JSON.parse('"' + jsonEscape(src) + '"') === src; // true

制御文字がきちんとエスケープされ、JSON.parse で完全に元へ戻る。絵文字(サロゲートペア)も壊れない。

非ASCIIを uXXXX にしたい場合

ASCIIしか通らない古い経路へ渡すなら、エスケープ後にさらに非ASCIIを \uXXXX へ変換する。ここはコードポイント範囲で機械的に置換すればよい。

1
2
3
4
5
6
function toUnicodeEscapes(s) {
return s.replace(/[^\x00-\x7f]/g, c =>
'\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4));
}
toUnicodeEscapes(jsonEscape('日本語'));
// "日本語"

まとめ

JSON文字列のエスケープは、\"\n だけを見て手書きの置換を並べると、バックスペースやフォームフィード・NULなどの制御文字やサロゲートペアで壊れる。JSON.stringify(s).slice(1, -1) に任せれば仕様どおり正しくエスケープでき、JSON.parse で往復も保証される。ブラウザ上で試せる形は JSON文字列エスケープ/アンエスケープ に置いた(処理はすべてブラウザ内で完結する)。


JSON文字列のエスケープは手書き置換で組まない:JSON.stringifyに任せる理由を手を動かして確かめる
https://blog.hashito.biz/2026/07/15/json-string-escape-stringify-pitfalls/
著者
hashito
作成日
2026年7月15日
著作権