JSONエスケープは「文字列を“JSONとして壊さない形”にする」技
業務でAPIを作ったり、フロントとバックエンドでJSONをやり取りしていると、
「文字列をJSONに埋め込んだらパースエラーになった」「改行やダブルクォートで壊れた」
みたいな経験をほぼ確実にします。
その原因の多くが、JSONエスケープを意識していないことです。
JSONの世界では、
「文字列はダブルクォート " で囲む」「中で使える特殊文字にはルールがある」
という決まりがあり、それを守るために必要なのが JSONエスケープ です。
JSON文字列のルールをざっくり押さえる
どんな文字をエスケープしないといけないのか
JSONの文字列は、必ずダブルクォートで囲みます。
"hello"
"改行あり\nテキスト"
"ダブルクォート: \" も書ける"
この中で、そのまま書くとJSONとして壊れるものがあります。代表的なのは次のようなものです。
ダブルクォート "
バックスラッシュ \
改行・タブなどの制御文字(\n, \r, \t など)
これらは、バックスラッシュ付きの「エスケープシーケンス」に変換して書く必要があります。
" → \"\ → \\
改行 → \n
タブ → \t
例えば、Javaの文字列 "A "quote" B" をJSONにするとき、
そのまま "A "quote" B" と書くと、どこまでが文字列か分からなくなって壊れます。
正しくは "A \"quote\" B" のように、
中の " を \" にエスケープしておく必要があります。
自前実装:シンプルなJSONエスケープユーティリティ
まずは「よく出る文字」だけをきちんと処理する
最低限、次の文字は確実にエスケープしたいところです。
ダブルクォート " → \"
バックスラッシュ \ → \\
改行 \n → \\n(Javaの文字列としては \n、JSONとしては \n)
復帰 \r → \\r
タブ \t → \\t
これをJavaで実装すると、こんな感じになります。
public final class JsonEscaper {
private JsonEscaper() {}
public static String escape(String text) {
if (text == null) {
return null;
}
StringBuilder sb = new StringBuilder(text.length() * 2);
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
switch (c) {
case '"':
sb.append("\\\"");
break;
case '\\':
sb.append("\\\\");
break;
case '\b':
sb.append("\\b");
break;
case '\f':
sb.append("\\f");
break;
case '\n':
sb.append("\\n");
break;
case '\r':
sb.append("\\r");
break;
case '\t':
sb.append("\\t");
break;
default:
if (c < 0x20) {
// 制御文字は \uXXXX 形式にする
sb.append(String.format("\\u%04x", (int) c));
} else {
sb.append(c);
}
}
}
return sb.toString();
}
}
Java使い方はこうなります。
System.out.println(JsonEscaper.escape("A \"quote\" B"));
// A \"quote\" B
System.out.println(JsonEscaper.escape("line1\nline2"));
// line1\nline2
System.out.println(JsonEscaper.escape("path\\to\\file"));
// path\\to\\file
Javaここで深掘りしたい重要ポイントは三つです。
一つ目は、「ダブルクォートとバックスラッシュを必ずエスケープしている」ことです。
JSONの文字列は " で囲まれるので、中に出てくる " は \" にしないと、
「ここで文字列が終わった」と誤解されてしまいます。
同様に、バックスラッシュはエスケープの開始記号なので、
そのまま書きたいときは \\ にしておく必要があります。
二つ目は、「改行やタブなどの制御文字を、対応するエスケープシーケンスに変換している」ことです。'\n' を \\n と書くことで、JSONとしても「改行」を意味する文字列になります。
(Javaのソースコード上では "\n" と書きますが、実際の文字列の中身は1文字の改行です。)
三つ目は、「0x20未満の制御文字を \uXXXX 形式で逃がしている」ことです。
JSON仕様では、制御文字はそのまま書かず、エスケープするのが安全です。
ここでは簡易的に、「見えない制御文字は全部 \u0000 形式にしてしまう」という方針にしています。
例題:JSONを手で組み立てたときに壊れるパターン
エスケープしないと「パースできないJSON」が簡単にできてしまう
例えば、こんなコードを書いたとします。
String message = "A \"quote\" B";
String json = "{ \"message\": \"" + message + "\" }";
System.out.println(json);
Java出力はこうなります。
{ "message": "A "quote" B" }
一見それっぽいですが、JSONとしては完全に壊れています。
どこまでが "message" の値なのか、パーサには分かりません。
これを JsonEscaper.escape を通すと、こうなります。
String message = "A \"quote\" B";
String escaped = JsonEscaper.escape(message);
String json = "{ \"message\": \"" + escaped + "\" }";
System.out.println(json);
Java{ "message": "A \"quote\" B" }
この形なら、JSONパーサは正しく "A \"quote\" B" を1つの文字列として扱ってくれます。
ここでのポイントは、「JSONを“文字列連結で手書きするとき”は、必ずエスケープを意識しないと壊れる」ということです。
そして実務では、そもそも“手書きしない”ほうが圧倒的に安全です。
実務では「JSONエスケープを書かない」ほうが正しい
ライブラリ(Jackson / Gson など)に任せるのが基本
SQLのときと同じで、
JSONも本来は 「自分でエスケープを書く」のではなく「ライブラリに任せる」 のが正解です。
例えば Jackson を使うと、オブジェクトをそのままJSONに変換できます。
import com.fasterxml.jackson.databind.ObjectMapper;
public class Message {
public String message;
}
ObjectMapper mapper = new ObjectMapper();
Message m = new Message();
m.message = "A \"quote\" B";
String json = mapper.writeValueAsString(m);
System.out.println(json);
// {"message":"A \"quote\" B"}
Javaここでは、"A "quote" B" の中の " を、
Jackson が自動的に \" にエスケープしてくれています。
このように、「JSONの仕様に詳しくなくても、安全なJSONを出せる」 のがライブラリの強みです。
自前でエスケープを書くのは、
テスト用にちょっとだけJSONを組みたい
ライブラリを使えない制約がある
JSONの仕様を学習したい
といった、限定的な場面にとどめるのが現実的です。
例題:ログやデバッグ用に「JSONっぽく出したい」とき
「完全なJSONではなく“JSON風”でよい場面」
ときどき、「ログにJSONっぽい形式で出したい」という場面があります。
String user = "Taro";
String action = "login";
String log = "{ \"user\": \"" + user + "\", \"action\": \"" + action + "\" }";
System.out.println(log);
Javaこの程度ならまだしも、
ユーザー名やメッセージに " や改行が入ってくると、すぐに壊れます。
そんなときに、JsonEscaper.escape を挟んでおくと、
「完全なJSONではないかもしれないけれど、少なくとも壊れにくい“JSON風ログ”」になります。
String user = "Taro \"Admin\"";
String action = "login\nsuccess";
String log = "{ \"user\": \"" + JsonEscaper.escape(user)
+ "\", \"action\": \"" + JsonEscaper.escape(action) + "\" }";
System.out.println(log);
// { "user": "Taro \"Admin\"", "action": "login\nsuccess" }
Javaここでのポイントは、「ログやデバッグ用でも、エスケープしておくと“後から機械で解析しやすくなる”」ということです。
JSONとして完全にパースできるかどうかは別として、
少なくともダブルクォートや改行で壊れない形にしておくと、後処理が楽になります。
まとめ:JSONエスケープユーティリティで身につけたい感覚
JSONエスケープは、「文字列をJSONの中に安全に埋め込む」ためのテクニックであり、
実装としては「" や \、制御文字をバックスラッシュ付きの表現に変える」だけのシンプルな処理です。
ただし、業務・実務で本当に大事なのは、
JSONを文字列連結で手書きしない
Jackson や Gson などのライブラリにシリアライズを任せる
それでも手書きするなら、必ずエスケープを通す
という設計のほうです。
もしあなたのコードのどこかに、
String json = "{ \"message\": \"" + userInput + "\" }";
Javaのような行があったら、
それを題材にして、
JsonEscaper.escape(userInput) を挟んでみる
あるいは、Map やクラスに詰めて Jackson で writeValueAsString してみる
というリファクタリングを試してみてください。
JSONエスケープを“書けるようになる”ことはゴールではなく、
「エスケープを意識しなくても安全なJSONを扱える設計にたどり着くための通過点」 です。
そこまで行けたとき、あなたの文字列処理は一段上の実務レベルに届いています。
