PHPで迂闊にJSONを返すとクライアントが死ぬ

ヨメレバCSS
オリジナルCSS

 クライアントとのやりとりで便利に使われるJSONですが、PHPから返すときにはいろいろと注意していないとクライアント側のパースで失敗して死んでしまいます。

 相手もPHPのように型付けがゆるい言語だったらまだ良いかもしれません。CやJavaのような静的型付け言語だとさらに厄介なことになります。

 環境はPHP 7.2.7です。またJSONは見やすいように改行とインデントを入れてあります。

スポンサーリンク
GoogleAdSence レクタングル(大)

json_encode を使用した場合の問題として

 PHPではjson_encodeを使うと、オブジェクトや配列からJSON文字列を作成してくれます。

 クライアントからJSON形式を求められたとき、大抵はこれを使用するのではないかと思います。

 この関数自体は便利なものですが、変数型に沿って自動変換をするためPHPのあいまいな型とJSONのしっかりした型が絶妙にアンマッチしてつらいことになります。

文字列型、数値型、booleanがブレる

 PHP内ではあまり意識することのない変数の型ですがJSONではきっちり区別されます。

 そしてPHP内で意識されないところなので、いつの間にか型が変わっていてもわかりません。

$val = [
        'value_int'=>1,
        'value_str'=>'1',
        'value_bool'=>true,
];

この$valをjson_encodeしてみると次のようになります。

{
  "value_int":1,
  "value_str":"1",
  "value_bool":true
}

まあそうですよね…となりますが、ここで思い出して欲しいのはPHPでコード書いてるときにこれらの型を意識しているかどうかです。

 例えばtrue/falseに1/0を入れるようなクセがあると、PHPのコードとしては問題ありませんが返却値がブレることになります。

 CSVファイルから読み込んだ数値から加工する場合も困ります。次のCSVファイルを読み込んで、

1,1
2,2
3,3

最初の値が2のときは+1するような処理をして、

<?php
$file = new SplFileObject("sample.csv");
$file->setFlags(SplFileObject::READ_CSV);
$datas = [];
foreach ($file as $line) {
    if ( $line[0] == 2 ) {
        $line[1] += 1;
    }
    $datas[] = $line;
}

$datasをjson_encodeして出力してみますとこうです。

[
  ["1","1"],
  ["2",3],
  ["3","3"]
]

おわかりいただけるだろうか。

 さらにnullに対する問題もあります。PHPでは割と「0と""とnullはまとめてfalse」みたいな扱いになっているところがありますが、JSONにすると明確に違います。

$val = [
        'value_int' => 0,
        'value_str' => '',
        'value_bool' => false,
        'value_null' => null,
];

これをjson_encodeしてみます。

{
  "value_int":0,
  "value_str":"",
  "value_bool":false,
  "value_null":null
}

 明確に別々になっていますね。

 PHP上でnullがとにかくふわっとしているので、空文字/falseだと思っていたら実はnullだったみたいなことはままあると思います(しPHP上ではあまり問題になりません)が、JSONで返すとクライアントが死にます。

対応策

 オプションなどでの対応はありません。

 個人的には次の点をフレームワークの出口などで自動変換すると良いと思います。

  • JSONで返す値はすべて文字列とする
  • booleanは"0"か"1"で返す
  • nullは””(空文字列)で返す

 クライアントとの決めごとになるうえ、一括でやらないと意味が薄い(どころか部分的にルールが適用されるので混乱する)ため後から適用することは厄介だと思います。

 また自動的に適用できる仕組みにしないと、結局どこかで対応が漏れる=バグになる可能性が常に内包されることになるためそれなら素直に適切な型を意識した方がマシでしょうか。

 対象の変数を再帰的にループして置き換えていくことになるためJSONの規模によっては負荷も気になりますので、部分的に適用しないとルール決めすることも必要かもしれません。

 nullに関してはオブジェクトを期待している場合などに空文字が入ってくるとそれはそれで厄介なので、クライアント側との相談で決めた方がよいところがあります。でも多分、PHP側としては全部空文字列に置き換えてしまって、クライアント側で必要に応じてnullにしたほうが面倒ごとが少ないのではと思います。

配列と連想配列がブレる

 これも割とクリティカルに死にます。

 PHPは配列と連想配列の扱いがあいまいで、配列操作をすると内部的に「数字をキーにした連想配列」に変わってしまうことがたまにあります。

 まず次のような簡単な配列を用意します。

$val = ['one', 'two', 'three'];

この$valをvar_dumpするとこう。

array(3) {
[0]=>
string(3) "one"
[1]=>
string(3) "two"
[2]=>
string(5) "three"
}

json_encodeするとこうです。

[
  "one",
  "two",
  "three"
]

 これを次のようにunsetして、「two」を消してみます。

$val = ['one', 'two', 'three'];
unset($val[1]);

するとvar_dumpの結果はこうなります。

array(2) {
[0]=>
string(3) "one"
[2]=>
string(5) "three"
}

json_encodeすると、さらにこうなります。

{
  "0":"one",
  "2":"three"
}

数字をキーにした連想配列に入れ替わっているんですね。

PHPとしては、まあ真ん中抜かれたらそうじゃろって感じで問題ありませんが、クライアント側で配列だと思って解釈すると落ちるみたいな可能性は高まります。

対応策

 json_encodeにはJSON_FORCE_OBJECT なるオプションがあります。これを指定してやると、配列をオブジェクトとして出力してくれます。

json_encode($val, JSON_FORCE_OBJECT )

とすることで、得られるJSONは次のようになります。

{
  "0":"one",
  "1":"two",
  "2":"three"
}

受け取る側でも備えておく

 こういったJSONを受け取るクライアントのほうでも、型に期待をせずに備えておくことが重要だと思います。

 特にC、Java、C#などの静的型付け言語で受け取る際には、値が決まった型で来ていない場合にどう解釈するのかを決めておいて、try-cacheなどでうまく丸めてやらないといけないと思います。いけないっていうか死にます。

そこでProtocol Buffersの登場かもしれない

 この問題はPHPやJSONの問題ではなく、異なるプラットフォーム間でデータをシリアライズ/デシリアライズする決めごとの話です。

 なので双方でライブラリをかっちり決めて、その通信フォーマットとしてJSONを使うなどすれば問題ではなくなります。なくなりますがあれ全部そうするのかーって思うとなあ。

 そのフォーマットを示し合わせる方法としてProtocol Buffersとかは検討してみるといいのかもしれませんし、むしろそのために出てきたものではないかと。

 実戦で使ったことがないので適しているかはわからないですが、こちらも調べてみると面白いかもしれないです。

スポンサーリンク
GoogleAdSence レクタングル(大)

シェアする

スポンサーリンク
GoogleAdSence レクタングル(大)