All Articles

PHPのjson_encode()で数値と文字列が混在して困った話

PHPのjson_encodeを普通に使ってたら問題があった

今運用中のサービスで、アプリエンジニアから時々アプリが落ちるとの報告。 原因を聞いてみると、「APIレスポンスのidフィールドが数値だったり文字列だったりする」らしくて、実際にdumpデータを見ると確かにそうなっている様子。 サーバ側は普通にDBの結果セットに対してjson_encode()を使ってるだけだし、今までそういう現象は起きてなかったのです。

原因調査

で、色々調べてみると、StakOverflowに以下のような投稿が。

PHP json_encode encoding numbers as strings

で、さらにリンクでPHPのバグじゃないか?って感じのものも。

Bug #40503 json_encode integer conversion is inconsistent with PHP

Submittedが2007年なので相当古いけど…。 ともあれ、原因をしっかり調査する時間もなく、アプリが落ちるのは緊急性が高いので、一旦PHPのバグだとしてコードレベルで対策。 PHP5.3以上では第二引数にJSONNUMERICCHECKをつけるのがまぁ常套手段ですかね。

<!--?php

...

// まとめてJSONレスポンスを出力するところ(コードは簡略化してます)
header("Content-Type: application/json");
echo json_encode($jsonData, JSON_NUMERIC_CHECK);

とりあえず凌げたかな、と思ってましたが、この時PHPにおける「NUMERIC」の扱いに気がつくべきだった…。

安易にJSON_NUMERIC_CHECKは指定すべきではない

is_numeric()という関数をご存知の方は多いと思いますが、それと同じく「数値っぽい文字列は全部数値に変換を試みる」という挙動なんですね。

で、このJSONNUMERICCHECKフラグを指定すると、JSONにエンコードするデータ全てにおいて、is_numeric()の挙動が発生します。それはもう、問答無用に。 これにより、JSONレスポンスのデータ内に10桁程度のHEX乱数のフィールドがあったんですが、そのデータに依ってはおかしな挙動をすることになりました。具体的には:

$jsonData = [
    "hexData": "25354255e7960" // 乱数生成されたもの
];

もうお分かりだと思いますが、"25354255e7960"というデータはis_numeric()ではtrueになります。よって数値に変換しようとしますが、数値で表現できる桁数をオーバーフローして変換に失敗します。 これは乱数なので、そういうデータが生成される「可能性がある」ということになり、さらに原因が見つけにくく混乱します。

というわけで、実装側ではDBの結果セットに対して数値を期待するカラムはちゃんとintにキャストする実装を入れて事無きを得ました。 もしも全APIに対しての修正が必要ということになってたら(最悪それも想定していた)もう目も当てられない。 ってことで、同じようなデータを扱ってる場合は注意が必要です。

まとめ

そもそもの問題として、このような現象がなぜ起きるのかは未だにわかっていません。json_encode()に渡すデータ量の問題なのか、PHPの実行メモリの問題なのか。はたまたキャッシュ併用の問題なのか。 もしもこの現象が起きる理由をご存知の方がいたら是非教えて下さい。

それにしても、上記のようなバグっぽい報告が上がっても追加フラグにて対応したりしてるのはさすがPHP…なのでしょうか。 あとDBの結果セットのintフィールドとかもデフォルトでは文字列になりますしね。

なんだかもやもやしたままの解決となってしまいましたが、ともかく動的型付け言語から静的型付け言語へJSONデータを渡すときは注意が必要だな、と。 もしも同じ現象で困ってしまった場合の参考になれば幸いです。

安易なJSONNUMERICCHECKはダメ。 現場からは以上です。