PHPのjson_encodeで日本語がエスケープされる|文字化けではない。JSON_UNESCAPED_UNICODEを付けると読める
json_encode() で日本語が記号の列になるのは、初期設定のエスケープで、文字化けではない。
第2引数に JSON_UNESCAPED_UNICODE を付ければ読める形で出る。
URLの / を消す定数と、途中で切れた日本語で false が返る落とし穴もまとめた。
PHPの json_encode() で日本語を出すと、「まる子」が \u307e\u308b\u5b50 のような記号の列になる。
これは文字化けではなく、日本語を \uXXXX の形に書き換える、という json_encode の初期設定だ。
読める形で出したいときは、第2引数に JSON_UNESCAPED_UNICODE を付ければいい。
この記事は、PHPの公式マニュアルを読んで、手元の PHP 8.3 で実際に動かして整理したものだ。
「僕は〜と思う」と書いたところだけが、僕の意見になる。
何も付けないと、日本語は \u の列になる
まず、何も指定しないで json_encode を呼んでみる。
$a = ['name' => 'まる子', 'url' => 'https://autocamp.website/maruko/'];
echo json_encode($a);出てくるのはこうなる。
{"name":"\u307e\u308b\u5b50","url":"https:\/\/autocamp.website\/maruko\/"}日本語が記号の列になっただけでなく、URLの / の前にも \ が入っている。
初めて見ると、壊れたように見える。
これは文字化けではない
PHPのマニュアルには、JSON_UNESCAPED_UNICODE の説明として「マルチバイト Unicode 文字をそのままの形式で扱います(デフォルトでは \uXXXX にエスケープします)」と書いてある。
つまり、何も付けないときに \uXXXX になるのは、決まりどおりの動きだ。
\u307e は「ま」を表す書き方で、JSONとしては正しい。
JavaScript の JSON.parse() や PHP の json_decode() で読み直せば、ちゃんと「まる子」に戻る。
だから、プログラム同士でやりとりするだけなら、このままでも困らない。
困るのは、人が読むときだ。
ログに書き出したJSONや、ページに埋め込んだ構造化データを目で確かめるとき、記号の列では中身が分からない。
直し方:JSON_UNESCAPED_UNICODE を付ける
第2引数に定数を渡すと、日本語がそのまま出る。
echo json_encode($a, JSON_UNESCAPED_UNICODE);
// {"name":"まる子","url":"https:\/\/autocamp.website\/maruko\/"}URLの \/ はまだ残っている。
こちらは JSON_UNESCAPED_SLASHES という別の定数で、マニュアルの説明は「/ をエスケープしません」。
2つは | でつないで一緒に渡せる。
echo json_encode($a, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// {"name":"まる子","url":"https://autocamp.website/maruko/"}手元で文字数を数えると、何も付けない形は74バイト、JSON_UNESCAPED_UNICODE を付けた形は65バイトだった。
日本語1文字が6バイトの記号の列になるか、UTF-8の3バイトのままか、の差だ。
日本語が多いデータほど、この差は大きくなる。
このサイトを動かしている自作のCMSのコードを検索してみたら、JSON_UNESCAPED_UNICODE は100を超えるファイルに書いてあった。
JSONを書き出すところでは、ほとんど毎回付けている計算になる。
僕は、日本語を扱うPHPなら、付けるのを基本にしておくほうが迷わないと思う。
もうひとつの落とし穴:日本語が途中で切れていると false が返る
json_encode のマニュアルには、すべての文字列データは UTF-8 でなければいけない、と書いてある。
戻り値は、成功すればJSONの文字列、失敗すれば false だ。
たとえば、「まる子」を substr() で4バイトだけ切り出すと、「る」の途中で切れる。
これを json_encode に渡すと、こうなる。
$bad = substr('まる子', 0, 4);
var_dump(json_encode(['n' => $bad])); // bool(false)
echo json_last_error_msg();
// Malformed UTF-8 characters, possibly incorrectly encodedfalse は echo すると何も表示されないので、APIの返事が空になっただけに見える。
エラーの文言は json_last_error_msg() で確かめられる。
切れた文字を置き換えて出したいなら、JSON_INVALID_UTF8_SUBSTITUTE(PHP 7.2.0 以降)がある。
マニュアルの説明では、不正な UTF-8 の文字を U+FFFD(置換文字)に変える。
手元では {"n":"ま�"} と出た。
失敗を見逃したくないなら、JSON_THROW_ON_ERROR(PHP 7.3.0 以降)を付けると、false を返す代わりに JsonException を投げてくれる。
僕は、黙って空になるよりは、例外で止まるほうが原因に早くたどり着けると思う。
バイト数で切ると日本語が壊れる話は、PHPのstrlenとmb_strlenの違いに書いた。
文字数で切りたいなら、substr() ではなく mb_substr() を使う。
まとめ
- json_encode で日本語が
\u307eのようになるのは、初期設定のエスケープで、文字化けではない - 読める形で出すなら
JSON_UNESCAPED_UNICODE、URLの\/も消すならJSON_UNESCAPED_SLASHESを一緒に付ける - 途中で切れた日本語を渡すと false が返る。
json_last_error_msg()で理由が分かる
出典
- 定義済み定数(JSON)- PHP マニュアル(JSON_UNESCAPED_UNICODE・JSON_UNESCAPED_SLASHES・JSON_INVALID_UTF8_SUBSTITUTE・JSON_THROW_ON_ERROR)
- json_encode - PHP マニュアル(UTF-8 であること・戻り値)
ほかにも書いています
受託・note・スタンプこういう仕組みを作る相談を受けています
毎日の更新・データ集め・集計の自動化/3万円〜収益がいくらだったか、どの施策が外れたか
note / 個人の視点で掘り下げていますスタンプラリーに挑戦する
行った場所は、現地チェックインで記録に残せます※受託の相談とnoteは、運営者が個人で受けている・書いているものです。
