PHPのjson_encodeで空白が返ってきてしまう場合のエラー確認方法
PHPでJSON形式のデータを作る際によく使うjson_encode関数ではエラー(=変換できない事象)が起こっても空文字が返ってくるだけなので、JSON_THROW_ON_ERRORオプションを付けて、エラーの理由を確認しましょーね、というだけのお話です。
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
最近これでハマって何時間も無駄にしてしまったので、備忘録として記事に残しておこうと思いました。
json_encodeの基本的な使い方
json_encodeは一言で言うと、「あらゆる変数をJSON(JavaScript Object Notation)形式にシリアライズする関数」です。シリアライズからわからない人はググってもらいたいですが、一応ざっくり説明すると「オブジェクトや配列をテキスト形式へ変換すること」です。
過去に「PHPでjson_encode/json_decodeする例」と題した記事を投稿していますが、具体的かつニッチな運用例で初心者向きではなかった気もするので、よりシンプルな例を掲載しておきます。
json_encodeの極シンプルなサンプル
<?php
$row['seq'] = 1;
$row['name'] = '山田太郎';
$row['kana'] = 'ヤマダタロウ';
echo json_encode($row);
?>
$rowという変数名にしていることからお察しの方もいるかと思いますが、データベースのテーブルから1行読み取り、それをJSON形式へ変換する、みたいな状況を想定して書きました。
実行結果
{"seq":1,"name":"\u5c71\u7530\u592a\u90ce","kana":"\u30e4\u30de\u30c0\u30bf\u30ed\u30a6"}
とまぁ、このように連想配列$rowをテキスト形式に変換することで、テキスト型のデータベース項目に格納するもヨシ、POSTデータとして使うもヨシ、JavaScriptとのXMLHttpRequest通信に使うもヨシ、活用方法が広がります。
がしかし、数値ならまだしもUTF8の項目をコードで格納されてしまうと、上記のとおり、「山田太郎」が「\u5c71\u7530\u592a\u90ce」という長ったらしいデータになってしまいます。
本来、「山田太郎」という文字はUTF8では1文字あたり3バイト。4文字分で合計12バイトです。それなのに上記例では1文字あたり6バイト必要なので、合計24バイトも消費してしまいます。短い文字なら気にならないかも知れませんが、これが100行分、1000行分となってくると無駄なデータ量が馬鹿にできません。
そんなわけで、日本語が含まれるデータをjson_encodeする場合には、JSON_UNESCAPED_UNICODEオプションを付けるのが一般的です。
json_encodeにJSON_UNESCAPED_UNICODEを付けた例
<?php
$row['seq'] = 1;
$row['name'] = '山田太郎';
$row['kana'] = 'ヤマダタロウ';
echo json_encode($row, JSON_UNESCAPED_UNICODE);
?>
実行結果
{"seq":1,"name":"山田太郎","kana":"ヤマダタロウ"}
データ量は少なくなったし、見た目もスッキリ。
json_encodeはエラーがあっても例外が発生しない
さて、こんな便利なjson_encodeですが、デフォルトでは変換エラーが発生しても何も言わずに空文字を返してしまう、という問題があります。
変数をJSON形式へ変換するだけなのにエラーが発生する状況なんてあるのか?と思われるかもですが、普通にありえます。
json_encodeでMalformedエラーが発生する例
<?php
$row['seq'] = 1;
$row['name'] = substr('山田太郎', 0, 10);
$row['kana'] = 'ヤマダタロウ';
echo json_encode($row, JSON_UNESCAPED_UNICODE);
?>
実行結果
<空>
このコードの実行結果は空、つまり変換エラーです。
変更したのはsubstr関数を使っている一か所のみ。「山田太郎」を先頭から10文字分切り取っています。
通常、Webで入力フォームを作る場合、文字列の長さを制限すると思います。メールアドレスやURLなら255文字以上は入力できないようにしたり、備考欄でも1000文字くらいを上限にしたり。
それ以上長い文字が渡されたら、データベースへ格納する前にちょんぎって格納するのも一般的ですよね。
そして、普通ならここでmb_substrという日本語に対応した文字列操作関数を使うべきところですが、元が海外製のソースコードで日本語が一切考慮されていなかったりすると、文字コードの途中でぶった切られてしまうんです。
これはRSSデータなどでもよく見られる現象です。海外製のアプリを改造して作ったブログシステム等、日本語が適切に処理できず、ブログの見た目は問題ないものの、出力されるRSSフィードでは日本語が途中でぶったぎられて文字化けしていたり、そんなデータは2022年現在でもたまに見かけます。
さて、上記の例に戻りますが、substr('山田太郎', 0, 10);は10文字目で切り取られるわけではなく、10バイト目でぶった切られるため、3文字分(=9バイト分)の「山田太」までは良いものの、「郎」の文字の途中で切り取られてしまうため文字化けが発生し、UTF8として正しくないコードになっているわけですね。
そのため、json_encodeは適切に変換できず、エラーが発生しているのですが、通知をしてくれないので、単なる空文字が取得される、という結果になっています。
json_encodeでランタイムエラーを発生させる例
<?php
$row['seq'] = 1;
$row['name'] = substr('山田太郎', 0, 10);
$row['kana'] = 'ヤマダタロウ';
echo json_encode($row, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
?>
冒頭に書いたとおりですが、json_encodeにJSON_THROW_ON_ERRORオプションを付けることで、変換に失敗した場合には例外エラーが発生するようになります。
実行結果
Fatal error: Uncaught JsonException: Malformed UTF-8 characters, possibly incorrectly encoded in json_encode.php:5 Stack trace: #0 json_encode.php(5): json_encode(Array, 4194560) #1 {main} thrown in json_encode.php on line 5
このとおり、Fatal errorが発生するため、json_encodeに渡したデータに問題があったことがすぐわかります。もちろん実運用ではtry~catchを付けるか、set_exception_handler関数などで、管理人へエラーが通知されるような仕組みにしておけば良いでしょう。
文字化けしたUTF8でも無理やりjson_encodeで変換したい
例外エラーが発生したことで、json_encodeに渡したデータに問題があることはわかりました。
「Malformed UTF-8 characters, possibly incorrectly encoded」とあることからUTF-8として正しくないデータを渡してしまっていることもわかりました。
…でも、どうしようもないこともありますよね?
先述したとおり、RSSフィード等、ほかのサイトからデータを取得してJSON形式へ変換している場合、元のデータ自体を直しようがありません。
ということで、少々文字化けしていようともjson_encodeで例外エラーを発生させずに無理やり通す例がこちら。
UTF8変換エラーを無視するオプションを付ける例
<?php
$row['seq'] = 1;
$row['name'] = substr('山田太郎', 0, 10);
$row['kana'] = 'ヤマダタロウ';
$row['name'] = mb_convert_encoding($row['name'], 'UTF-8', 'UTF-8');
echo json_encode($row, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE);
?>
実行結果
{"seq":1,"name":"山田太","kana":"ヤマダタロウ"}
JSON_INVALID_UTF8_IGNOREを付けることで、UTF8として正しくないデータは無視されるため、「山田太」というデータが格納されます。
事前にmb_convert_encodingでUTF8へ変換しておく例
<?php
$row['seq'] = 1;
$row['name'] = substr('山田太郎', 0, 10);
$row['kana'] = 'ヤマダタロウ';
$row['name'] = mb_convert_encoding($row['name'], 'UTF-8', 'UTF-8');
echo json_encode($row, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
?>
実行結果
{"seq":1,"name":"山田太?","kana":"ヤマダタロウ"}
追加したのはmb_convert_encodingの1行のみ。UTF-8からUTF-8へ変換するという、一見無意味なコードですが、mb_convert_encodingは変換できなかった文字を「?」記号に変換してくれるため、どこでエラーが発生したのか、見た目にわかりやすいというメリットがあります。
JSON_INVALID_UTF8_IGNOREで変換エラーを無視するもヨシ、mb_convert_encodingで文字化けの箇所をわかりやすくするもヨシ、要件によって使い分けると良いでしょう。
まとめ
json_encodeで空白が返ってくる場合はJSON_THROW_ON_ERRORを付けて例外エラーが発生していないか確認しよう、というだけの話ではすぐ終わってしまうので、json_encodeの基本的な使い方と、文字化けしているデータでも無理やり通す方法について解説してみました。
ここで紹介したのはぼくが運用しているWebサイトで実際に発生したトラブル例です。外部サイトからのデータ取り込み時に文字化けデータが送り込まれてしまい、JSON変換できずに想定外のエラーが発生してしまったんですね。
最初はなぜJSON変換すると空白になってしまうのかちっともわからず何時間も悩んでしまいましたが、まさかデフォルトではjson_encode関数は例外エラーを発生しないとは…。
しばらくしたら、このことも忘れてしまいそうなので、こうして記事として残しておきます。ついでに同じようなトラブルで困っている方のお役に立てば幸いです。