JSONエスケープ
JSONのエスケープは、そのままだと文字列を終了させたり壊したりする文字を書き換えます。RFC 8259が必須としているのは2つのエスケープ——"と\——で、さらに5つの名前付き制御文字エスケープ、任意のスラッシュエスケープ、そして万能の\uXXXX形式が加わります。このガイドでは、すべてのエスケープと、絵文字の背後にあるサロゲートペア、そしてエスケープが役立つ場面と害になる場面を扱います。
JSONエスケープとは
JSONの文字列はすべて2つのダブルクォートの間にあります。だからこのフォーマットには、文字列を終わらせずに文字列内にダブルクォートを入れる方法と、意図しないエスケープを開始せずに文字列内にバックスラッシュを入れる方法が必要です。その答えがエスケープです。文字の前に置かれたバックスラッシュは、パーサーにとってその文字の意味を変え、構造上の記号を普通のデータに変えます。
この仕組みは小さく、閉じたものです。RFC 8259が定義するのは、2文字のエスケープがちょうど9個と、拡張形式の\uXXXXだけ。それ以外はありません。バックスラッシュの後にその他の任意の文字が続くと、寛容な素通しではなく構文エラーになります。この閉じていることは特徴です。仕様に適合するパーサーは、そのシーケンスを認識するか文書を拒否するかのどちらかであり、エスケープのバグは常に再現できます。
このガイドでは、エスケープの一覧表を一通り見たうえで、人が最もつまずく2つの部分——絵文字のために必要になる\uXXXXとサロゲートペア、そして非ASCIIテキストのエスケープにおけるサイズのトレードオフ——を扱います。すべてのサンプルはコピーでき、リンク先のツールはブラウザ内で動作します。何もインストールせずに、手持ちの文字列をローカルで往復させて確かめられます。
エスケープの働きを一番手っ取り早く確かめるには、パーサーに解決させるのを見ることですJSONフォーマッターは、ドキュメントのインデントを整えながら、入力の隣にエスケープ解除後の結果を表示します。
エスケープは文法の一部にすぎません。残りのルールはJSON構文チートシートにまとまっています。構造、ホワイトスペース、数値を含む文法の残りが、1ページのリファレンスに収まっています。
絶対に省略できない2つのエスケープ
文字列の中では、リテラルのダブルクォートは"として、リテラルのバックスラッシュは\\として書かなければなりません。この2つは、すべての生成者にとって必須です。これらがなければ、パーサーは文字列がどこで終わるのか、エスケープがどこから始まるのかを判別できません——代わりの書き方が存在しないためです。文法のその他のエスケープは、その文字をリテラルで書くこともできたという意味でしか任意ではありません。この2つには、文字列内で使えるリテラル表現がまったくないのです。
フォワードスラッシュは特殊なケースです。\/は合法で/を意味しますが、リテラルのスラッシュをエスケープする必要は決してありません。この短縮形が存在するのは1つの歴史的な理由のためです。HTMLの<script>ブロックにJSONを埋め込む際、文字列内の</script>という並びが、囲んでいるタグを早く閉じてしまう可能性がありました。今でも防御的にすべてのスラッシュをエスケープする生成器があります。その代償は、少し大きく少しノイズの多いドキュメントです。
1つの文書に含まれる必須と任意のエスケープ
{
"path": "C:\\temp\\notes.txt",
"line": "she said \"ship it\"",
"html": "<\\/script>"
}パーサーが返すもの
path: C:\temp\notes.txt line: she said "ship it" html: </script>
名前付きの5つの制御文字
テキストは、U+0020未満の制御文字——タブ、改行、復帰などの類——を合法的に含められます。しかし、JSON文字列内の生の制御文字は相互運用に適しません。行単位で処理するツールや一部のパーサーを直撃で壊します。そのため規格は制御文字のエスケープを要求し、そのうち5つに名前を与えています。\bはバックスペース(U+0008)、\fはフォームフィード(U+000C)、\nはラインフィード(U+000A)、\rはキャリッジリターン(U+000D)、\tは水平タブ(U+0009)です。その他の制御文字は\u00XX形式、たとえば\u0001で書かなければなりません。
改行について特に何を意味するかに注目してください。整形されたドキュメントはトークン間に実際の改行を持ちますが、文字列値の中の改行は常に\nという2文字です。文字列値は、内容が複数行であっても、ファイル内では1行を占めます。整形されたJSONを読むときに最もよくある混乱ポイントの1つです。
| エスケープ | コードポイント | 文字 |
|---|---|---|
| \b | U+0008 | バックスペース |
| \t | U+0009 | 水平タブ |
| \n | U+000A | ラインフィード(改行) |
| \f | U+000C | フォームフィード |
| \r | U+000D | キャリッジリターン |
| \u00XX | U+0000–U+001F | その他の任意の制御文字 |
1つのJSON文字列に保存された3行のメモ
{
"note": "first line\nsecond line\ttabbed"
}パース後の内容
first line second line tabbed
\uXXXX:任意のコードポイント、ちょうど4桁の16進数
\uエスケープは、コードポイントをちょうど4桁の16進数で書きます。\u00e9はé、\u4f60は你、\u0041はリテラルで書いてもよかったのと同じAです。パーサーはエスケープ形式とリテラル形式を同一視します。すべての非ASCII文字をエスケープした文書も、必須の2つしかエスケープしない文書も、まったく同じ文字列にデコードされます。エスケープはデータの違いではなく、転送時の判断です。
4桁という制約により、この形式はU+FFFFまでのコードポイント、つまり基本多言語面までしか扱えません。それより上のもの——絵文字、歴史的な文字体系、ほとんどの記号——は1つの\uエスケープに収まりません。この限界こそ、サロゲートペアが登場する場所です。
同じ値の2つの書き方
{
"city": "\u5317\u4eac",
"note": "escaped form"
}
{
"city": "北京",
"note": "literal UTF-8"
}どちらも同じ文字列にパースされる
city: 北京 (エスケープしたファイルはASCIIのみ。 そのままのファイルは小さいがマルチバイト)
追加面:サロゲートペアと孤立サロゲート
内部的に、JavaScriptと\u表記はUTF-16コードユニットで動きます。U+FFFFより上の文字はペアで書かれます。U+D800–U+DBFFのハイサロゲートに、U+DC00–U+DFFFのローサロゲートが続く形です。したがって、ニコニコ顔のU+1F600は\ud83d\ude00になります——1つの絵文字に12文字のソースです。片方だけをエスケープすると孤立サロゲートになります。RFC 8259 §8.2は、そのような文字列を解釈が予測できないものとしてマークしており、相互運用可能な生成器はそもそも出力しません。
サロゲートペアは、文字列の長さが嘘をつく理由でもあります。JavaScriptは"😀"の長さを2と報告します。文字ではなくUTF-16コードユニットを数えているからです。エスケープされたテキストを切り出したり切り詰めたり検証したりするときに問題になる細部であり、書記素を認識するライブラリが存在する理由でもあります。
絵文字1つ、コードユニット2つ、エスケープ後は12文字
{
"mood": "\ud83d\ude00"
}
{
"mood": "😀"
}どちらも同じ1文字にパースされる
mood: 😀 (U+1F600――length 2 in UTF-16 units)
エスケープの代償:可読性とサイズ
エスケープは、可読性を安全性と引き換えにします。すべての非ASCII文字をエスケープした文書は、どんな7ビットチャンネルでも生き残れます。各バイトの第8ビットを落とすゲートウェイ、レガシーなメールシステム、純粋なASCIIとして設定されたログ——その代償は、人間にはほとんど読めないことです。これは初期の多くのJSONライブラリの既定姿勢でした。現代の既定値はリテラルのUTF-8を好み、文法が要求するものだけをエスケープします。
サイズの代償はコードポイントから予測できます。\uXXXXエスケープは1つにつき、何をエンコードしていようと正確に6バイトだからです。CJK主体の文書をアンエスケープすると、およそ半分になります。アラビア文字やキリル文字のテキストは約3分の1まで縮みます。そして、素のASCIIをエスケープすると6倍に膨れます——だからこそ、古いシステムが作ったASCIIエスケープ済みのペイロードは、実際の中身に比べてずいぶん太って見えるのです。
| テキスト | コードポイント | リテラルUTF-8 | エスケープ形式 | エスケープ後のサイズ |
|---|---|---|---|---|
| a | U+0061 | 1バイト | \u0061 | 6バイト |
| ب | U+0628 | 2バイト | \u0628 | 6バイト |
| а | U+0430 | 2バイト | \u0430 | 6バイト |
| 你 | U+4F60 | 3バイト | \u4f60 | 6バイト |
| 😀 | U+1F600 | 4バイト | \ud83d\ude00 | 12バイト |
アンエスケープ、往復変換、エスケープを保つべきとき
アンエスケープは、自分で書くべき別個の操作ではありません。仕様に適合するパーサーはすべて、デコードの最中にそれを実行します。"\u0041"をパースするのも"A"をパースするのも、同じ文字列が得られます。逆方向——JSONを生成するときに何をエスケープするかの選択——はライブラリごとに好みが分かれるところであり、ここでの唯一の強制ルールは、2つの必須エスケープと制御文字の要件だけです。
過剰なエスケープを続ける価値があるのは、正当な2つのユースケースだけです。UTF-8を信用できないチャンネルを生き延びるため、そして、目に見えない符号化の違いをすべて可視化すべきdiffのためです。それ以外では、最小限のエスケープにしたリテラルUTF-8のほうが、小さく、読み取りが速く、レビューしやすくなります。受け取った文書が過剰にエスケープされているように見えたら、一度パースして再シリアライズしてください。出力は任意のエスケープをすべて落としながら、データはまったく同じままです。
よくある質問
機密性の高い文字列を、このサイトのJSONツールに貼り付けても安全ですか?
はい。このガイドからリンクされたフォーマッター、バリデーター、ミニファイアーは、すべてブラウザ内でローカルに動作します。アップロードの工程はありません。パースと再シリアライズはタブ内のJavaScriptで行われ、Networkパネルにテキストを運ぶリクエストは表示されず、サイトのContent-Security-Policyが外向きの接続を遮断して二重の防御になります。文字列は、入力したマシンから出ていくことはありません。
JSON文字列に絵文字を入れるにはどうすればよいですか?
方法は2つあります。絵文字をリテラルで貼り付ける(😀は有効なUTF-8のJSONです)か、そのサロゲートペアである\ud83d\ude00を書くかです。どちらも同じ文字にデコードされます。エスケープ形式を知っておく価値はあります。一部のチャンネルや古いツールはリテラルの絵文字を壊すことがあり、エラーメッセージ、ログ、diffでは、顔そのものではなくペアで表示されることが多いからです。
孤立サロゲートとは何ですか?なぜパーサーを壊すのですか?
パートナーなしで書かれた、ハイサロゲート(\ud800–\udbff)またはローサロゲート(\udc00–\udfff)のことです。単独では何もエンコードしません。RFC 8259 §8.2は、それを含む文字列を解釈が予測できないものとしています。実装によって挙動はまちまちです——受け入れるもの、U+FFFDに置き換えるもの、例外を投げるもの——なので、ペアの半分を出力してはいけませんし、入ってきたデータの孤立サロゲートは破損として扱ってください。
/をエスケープする必要がないのに、\/は有効なエスケープなのはなぜですか?
JSONをHTMLに埋め込むためのものだからです。<script>ブロック内では、文字列中のリテラルの</script>という並びがブロックを早く終わらせてしまうため、生成器はスラッシュを\/とエスケープして、このパターンを崩してきました。文法はエスケープしない形も等しく受け入れます。HTMLにJSONを埋め込むのでないかぎり、\/は純粋なノイズです——ミニファイアーは喜んでこれを取り除きます。
エスケープすると、APIが受け取るデータは変わりますか?
いいえ。エスケープは書き方であって値ではありません。"\u00e9"も"é"も同じ文字列にデコードされるため、仕様に適合するパーサーなら、どちらでも同じUnicodeテキストを渡します。エスケープが変えるのは、ファイルの見た目とサイズだけです。だからこそ、過剰にエスケープされたレスポンスは見苦しくても無害であり、パースして再シリアライズすれば正規化できます。
\uエスケープのないJSONは、どれだけ小さくなりますか?
文字体系によります。\uXXXXは1つにつき6バイトですが、リテラルのUTF-8はASCIIなら1バイト、アラビア文字やキリル文字なら2バイト、CJKなら3バイト、絵文字なら4バイトだからです。中国語の文書をアンエスケープするとおよそ半分になり、アラビア文字やキリル文字は約3分の1まで縮み、ASCIIのエスケープは6倍に膨れます。エスケープ形式は常に、同等かそれ以上の大きさです。
ここで引用した規格はどこで読めますか?
- RFC 8259 — データ交換フォーマットJavaScript Object Notation(JSON)