Java Tips | 文字列処理:JSONエスケープ

Java Java
スポンサーリンク

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を扱える設計にたどり着くための通過点」 です。
そこまで行けたとき、あなたの文字列処理は一段上の実務レベルに届いています。

タイトルとURLをコピーしました