ブログ

JSONエスケープ完全ガイド

エスケープは、すべての開発者がいつか直面するトピックです。たいていは何かが壊れてフラストレーションを感じたときに初めて意識します。正しく見えるJSON文字列がパーサーに拒否される、改行を含むはずの文字列が\nをそのまま表示する、正しく保存されたはずの値がAPIを往復した後に文字化けして戻ってくる。こうした問題のほぼすべてはエスケープに行き着きます。このガイドでは、エスケープが必要な理由、エスケープが必要な文字、そしてさまざまな言語やコンテキストで正しく処理する方法を解説します。

エスケープが必要な理由

JSON文字列はダブルクォート文字で区切られます。これはすぐに問題を引き起こします。文字列自体にダブルクォートが含まれている場合はどうすればいいのか?エスケープなしでは、パーサーは内側のダブルクォートを文字列の終端と解釈してしまいます。

// 無効 — 内側のクォートが文字列を途中で終わらせる
{ "message": "She said "hello" and left" }
// 有効 — 内側のクォートがエスケープされている
{ "message": "She said \"hello\" and left" }

ダブルクォートの他にも、構造的または安全上の理由からエスケープが必要な文字があります。

  • 改行、タブ、キャリッジリターンなどの制御文字はJSON値の1行表現を壊す
  • バックスラッシュ自体はエスケープ文字なので、エスケープシーケンスと区別するためリテラルのバックスラッシュをエスケープする必要がある
  • 非ASCII Unicodeは、Unicodeを正しく処理できないシステムを通じた安全な転送を保証するためエスケープできる

JSONエスケープシーケンスリファレンス

エスケープシーケンス文字Unicodeコードポイント
"ダブルクォートU+0022
\バックスラッシュU+005C
/スラッシュ(オプション)U+002F
\bバックスペースU+0008
\fフォームフィードU+000C
\n改行(ラインフィード)U+000A
\rキャリッジリターンU+000D
\t水平タブU+0009
\uXXXXUnicode文字(16進数4桁)任意のBMPコードポイント

スラッシュ(/)はオプションでエスケープ可能です。\//はどちらもJSONで有効です。これはHTMLの<script>タグ内にJSONを埋め込む際に</がスクリプトブロックを終わらせてしまうという歴史的な理由から存在します。

Unicodeエスケープ

任意のUnicode文字は\uXXXXエスケープシーケンスを使ってJSONで表現できます。XXXXは4桁の16進数コードポイントです。

{
"greeting": "\u0048\u0065\u006C\u006C\u006F",
"copyright": "\u00A9 2024",
"checkmark": "\u2713"
}

パース後、"\u0048\u0065\u006C\u006C\u006F"は文字列"Hello"になります。Unicodeエスケープは、高バイト文字を破損させる可能性のあるシステムを通じた転送でJSONドキュメントにASCII文字のみを含めたい場合に便利です。

U+FFFFを超える文字のサロゲートペア

基本多言語面(BMPの外、コードポイントがU+FFFFを超える)の文字、例えば絵文字や稀なCJK文字は、JSONの\uXXXX表記でサロゲートペアが必要です。

{
"emoji": "\uD83D\uDE00"
}

2つの\uシーケンスが合わさって単一の文字😀(U+1F600)を表します。ほとんどのJSONシリアライザーはこれを自動的に処理しますが、JSONを手動で構築する場合は、単一の高いコードポイントが2つの\uエスケープシーケンスを必要とすることを覚えておいてください。

モダンなJSONパーサーは文字列内に直接のUTF-8エンコードされた文字も受け入れるため、ファイルがUTF-8エンコードされていれば"emoji": "😀"は有効なJSONです。

よくあるエスケープの落とし穴

二重エスケープ

二重エスケープは、すでにエスケープされた文字列を再度エスケープしたり、システムの複数のレイヤーでエスケープを適用した場合に発生します。典型的な例はJSON文字列フィールド内にJSONを格納する場合です。

// JSON文字列フィールド内にJSON文字列を値として持つ外側のJSON
{
"payload": "{\"key\": \"value\"}"
}

この値を受け取ったシステムが保存前に再びエスケープしようとすると、バックスラッシュがエスケープされます。

{
"payload": "{\\\"key\\\": \\\"value\\\"}"
}

2回アンエスケープすれば元の値に戻りますが、1回だけでは壊れた文字列になります。修正方法は、ちょうど1つのレイヤーでエスケープし、ちょうど1つのレイヤーでアンエスケープすることです。すでにエスケープされている値を再度エスケープしないでください。

JSONエスケープとURLエンコードの混同

JSONエスケープとURL(パーセント)エンコードは完全に異なるメカニズムです。スペースはURLでは%20ですが、JSON文字列では" "(エスケープ不要)です。スラッシュはURLパスセグメントでは%2Fですが、JSONでは/(またはオプションで\/)です。

JSONに埋め込む前に値をURLエンコードすると、二重エンコードされたゴミが生成されます。

// 誤り: JSONシリアライゼーション前に値をURLエンコード
const value = 'hello world';
const wrong = JSON.stringify(encodeURIComponent(value));
// 結果: "\"hello%20world\"" — %20はJSONのリテラルテキスト
// 正しい: JSON.stringifyにエスケープを任せる
const correct = JSON.stringify(value);
// 結果: "\"hello world\"" — スペースはJSON文字列で有効

エスケープされていない制御文字

JSONは制御文字(U+0000からU+001F)をエスケープすることを要求します。JSON文字列に直接埋め込まれた生の改行やタブ文字は技術的に無効です。一部のパーサーは許容しますが。

// 無効 — 文字列値内の生の改行
{ "note": "line one
line two" }
// 有効 — エスケープされた改行
{ "note": "line one\nline two" }

言語別のエスケープ処理

JavaScript

JavaScriptのJSON.stringify()は必要なエスケープをすべて自動的に処理します。JavaScriptでJSON文字列を手動で構築すべきではありません。

const data = {
message: 'She said "hello"',
path: 'C:\\Users\\alice',
multiline: 'line one\nline two',
};
const json = JSON.stringify(data, null, 2);
// 正しくエスケープされたJSONを自動生成

JSON.parse()も自動的にアンエスケープします。

const parsed = JSON.parse('{"path": "C:\\\\Users\\\\alice"}');
console.log(parsed.path); // C:\Users\alice

JavaScriptの文字列リテラル自体で追加のエスケープが発生することに注意してください。"\\\\"は4文字のJavaScript文字列で2つのバックスラッシュを含み、これがJSONの1つのエスケープ済みバックスラッシュを表し、パースすると1つのバックスラッシュになります。

Python

Pythonのjsonモジュールも同様に機能します。json.dumps()json.loads()を使い、JSONを手動で構築しないでください。

import json
data = {
"message": 'She said "hello"',
"path": r"C:\Users\alice",
"multiline": "line one\nline two"
}
json_string = json.dumps(data, ensure_ascii=False)
# ensure_ascii=Falseは\uXXXXシーケンスにエスケープせず
# Unicode文字をそのまま保持する
# アンエスケープ
parsed = json.loads('{"path": "C:\\\\Users\\\\alice"}')
print(parsed["path"]) # C:\Users\alice

ensure_ascii=Falseパラメーターは覚えておく価値があります。デフォルトでは、Pythonのjson.dumps()はすべての非ASCII文字を\uXXXXシーケンスにエスケープします。対象システムがUTF-8を処理できると確信している場合、ensure_ascii=Falseを設定するとより読みやすい出力が得られます。

テンプレート文字列でのエスケープ

HTML、SQL、シェルスクリプト内にJSONを埋め込む際は、JSONと外側のコンテキストの両方でエスケープする必要があります。最も安全なアプローチは、すでにシリアライズされたJSONに対して外側の言語のエスケープ機構を使うことです。

// HTMLデータ属性にJSONを埋め込む
const jsonValue = JSON.stringify({ key: 'value with "quotes"' });
const htmlAttribute = `data-config="${jsonValue.replace(/"/g, '&quot;')}"`;

実践的なエスケープのヒント

  1. 常にライブラリのシリアライザーを使う — 連結でJSON文字列を構築しないでください。主要な言語にはすべてエスケープを正しく処理する標準JSONライブラリがあります。

  2. エンコードレイヤーを確認する — 値が二重エスケープされているように見える場合、各シリアライゼーションとデシリアライゼーションのレイヤーをトレースして、エスケープが複数回適用されている箇所を見つけます。

  3. パスにはrawの文字列リテラルを使う — Pythonでは、Windowsパスの意図しないエスケープシーケンスを避けるため、rawの文字列(r"C:\Users\alice")またはスラッシュ("C:/Users/alice")を使います。

  4. 手動構築後はバリデーションする — シェルスクリプトなどでどうしてもJSONを手動で構築する必要がある場合は、使用前にパーサーで結果を検証します。

  5. エンコードを明示する — ファイルにシリアライズする際は、Unicode文字が一貫して処理されるようシステムデフォルトに頼らず明示的にUTF-8エンコードを指定します。

エスケープ・アンエスケープツールの活用

2回シリアライズされたJSON文字列(クォートされエスケープされたJSON文字列自体がJSONの値になっている)を受け取り、読みやすい形式にアンエスケープする必要がある場合があります。または、特殊文字を含む生の文字列をJSONドキュメントに安全に埋め込む必要がある場合もあります。

エスケープ・アンエスケープツールはパースコードを書くことなく、こうした変換を即座に処理します。

JSONKitのEscapeツールでは生のテキストを貼り付けて正しくエスケープされたJSON文字列表現を取得したり、エスケープされたJSON文字列を貼り付けてアンエスケープされた元のテキストを取得したりできます。すべての処理はブラウザ内で行われ、データはサーバーに送信されません。

まとめ

JSONエスケープはすべての標準JSONライブラリによって自動的に処理されます。これが最も重要なポイントです。JSON.stringify / json.dumps / JsonSerializer.Serializeを使い、ライブラリにエスケープを任せてください。JSONの手動構築がほとんどのエスケープバグの根本原因です。

エスケープの問題(二重エスケープ、生の制御文字、エンコードレイヤーの混乱)が発生した場合は、各シリアライゼーションステップを体系的にトレースしてください。このガイドのエスケープシーケンスリファレンスはJSON仕様が定義するすべてのシーケンスをカバーしています。