JSON::XS 4.0の変更点と、それにともなうJSON、JSON::PPの変更について

これはPerl Advent Calendar 2018 7日目の記事です。

tl;dr

先日リリースされたJSON::XS 4.0が2013年に行われたJSONの仕様変更に追随したので、decode_json($json) の結果はかならずしもリファレンスではなくなりました。encode_json($value) にもリファレンス以外の値を受け入れるようになっています。結果、JSONモジュールの挙動がバックエンドによって異なる状態になり、各地でテストがこけるなどの影響が出ました。これを受けてJSONJSON::PP側も変更に追随しましたが、みなさまの方でも適宜ご対応をお願いします。

2018年11月16日付けでJSON::XSのバージョン4.0が出ました。3.0が出たのが2013年10月29日のことですから、5年振りの大型アップデートということになります。今回の目玉は二点。いずれもこの数年界隈で何度となく話題に上がっていたものです。

allow_nonrefがデフォルトで有効に

配列やハッシュにくるまれていない素のスカラーを受け付けるallow_nonrefというオプションは2007年にJSON::XSがリリースされた当初から存在していたのですが、これはあくまでもRFC4627 (2006年)に対する拡張の扱いで、デフォルトでは無効になっていました。

その後、2013年にRFC7158が出てJSONの仕様が変わり、素のスカラーJSONの値として有効なものとなりますが、JSON::XSは(そしてJSONJSON::PPも)セキュリティ上の懸念があるとしてこの仕様変更には追随しませんでした。JSONJSON::PP側にも仕様変更に追随するよう問題提起されていましたが、もちろんJSON::XS側にも同じような要請はあったのでしょう。2016年にリリースされたJSON::XS 3.02では新旧の仕様に関してわざわざドキュメントに一節を設け、

JSON::XS will not allow scalar data in JSON texts by default

と態度表明をしていました。だから、私自身この変更を見たときにはかなり驚いたのですが、よく見てみると、その節の最後にはこのような留保も書いてありました。

future versions might/will likely upgrade to the newer RFC as default format, so you are advised to check your implementation and/or override the default with ->allow_nonref (0) to ensure that future versions are safe.

そして今回、この助言に従っていなかったモジュール、アプリケーションが仕様変更の影響を受けている、ということになります。

もっとも、問題になっているといっても、今回の変更はこれまでエラーとなっていたJSONやデータを受け付けるようになった、というもの。互換性のためにあえてそのようなデータ/JSONがエラーになることを確認するテストを書いていた場合、あるいはソケットなどを経由して垂れ流したJSONデータをインクリメンタルパーサーでデコードしていたという場合はともかくとして、一般的な用法の範囲内では、既存のデータ、既存のJSONが受け付けられなくなる、ということはありませんので、まずはご安心をば。

ただ、デコード時のエラーチェックが不足していたり、デコードしたデータがかならずリファレンスであると仮定しているようなコードでは、想定外の入力が入ってきて予期せぬエラーが発生することがありえるのでご注意ください。

既存の挙動を変えたくない場合は、encode_json/decode_jsonのようなラッパーをそのまま使うのはやめて、JSON::XS->new->utf8->allow_nonref(0)->encode/decodeのようにするか(もちろん可能ならJSON::XS->new->utf8->allow_nonref(0)の部分は何かの変数にキャッシュしておくべきでしょう)、いっそのことJSON::XS/JSON::PP 4.0に明示的に依存するようにして、テストもallow_nonrefが有効になっていることを前提にするのもありかと思います(4.0より前のJSON::XSはすでにCPANから削除されているため、明示的に指定しない限り、JSON::XSに依存しているモジュールがあれば現状ではJSON::XS 4.0がインストールされます)。

この変更によって、かれこれ三週間ほどJSON 2.97001のテストがこけ続け、CI環境や各種スモーカーを運用しているみなさまにはご迷惑をおかけしていたのですが、これを機にJSONおよびJSON::PPも同様にallow_nonrefを有効にした4.0をリリースしました。一部の界隈からはJSON::PPまで上げる必要はなかったのではないかという声も聞こえてきますが、そもそもJSON::PP側が待っていたのはJSON::XSが仕様変更に追随していなかったからですし、JSON::XSとJSONJSON::PPのメンテナ間ではなるべくお互いの互換性を維持しようという合意もできているので、この後に及んでJSON::PPだけあえて十年以上昔の古い仕様にこだわり続ける理由もないと思っています(この年末の忙しいときにこんなことはしたくなかったという思いはありますが、このタイミングを逃すと今度はPerl 5.30のコードフリーズが来るんですよね…)。

なお、JSON 4.0は、テストに使うJSON::backendPPモジュールがJSON::PP 4.0相当になっているだけで、実際にはJSON::XS 2〜4、JSON::PP 2.90〜4のいずれがバックエンドになっても動作します(Cpanel::JSON::XSも一部の非互換部分を除いては動作しますが、推奨しません。Cpanel::JSON::XSを使うのであれば、ラッパを使わず直接使うことをおすすめします)。

また、本項執筆時点ではCpanel::JSON::XSは今回の変更に追随していません。そのため、JSON::MaybeXSを利用して互換性のテストを行っているモジュールの中にはこの影響を受けてテストがこけているものが出ているかもしれません(メンテナ陣には問題が伝わっているので、この週末には必要に応じてなんらかの対策が取られるかもしれませんが、現時点でははっきりしたことはお伝えできません)。また、Cpanel::JSON::XSは後述するまったく別の理由で独自にバージョン4にしてしまったため、現状Cpanel::JSON::XSとJSONを共用している方は、この変更の有無をバージョンからは簡単に判別できない状態になっています(JSON::XSとJSON::PPはメジャーバージョンと主要な機能の対応が取れているので、JSON->backend->VERSION >= 4で判別できるのですが、JSONのバックエンドがCpanel::JSON::XSになるとこの条件では正しく判定できません)。Cpanel::JSON::XSをご利用の方はご注意ください。

boolean_values

JSON::XS 4.0では新たにboolean_valuesというメソッドが導入されました。これはJSONをデコードしたときにtrue/falseの値を任意のオブジェクトに変更できる、というものです。

これは、JSON単体で使っている分には特に意味のない機能ですが、JSONでデコードしたデータをたとえば何か別のシリアライザに引き渡したいとき、このboolean_valuesであらかじめbooleanやData::MessagePack::Booleanのオブジェクトを渡しておくと、データ構造の中にtrue/falseがあったらJSON::PP::Booleanのオブジェクトではなく、該当のオブジェクトに置き換わるので、YAMLやMessagePackでそのままシリアライズしやすくなる、と。

もっとも、この機能が有効なのはデコードのときのみなので、booleanのようにJSON::PP::Booleanとの互換性がないモジュールの場合、該当のデータ構造をJSONに戻す場合はconvert_blessedなどのお世話になることになります。JSON::PP::Booleanと互換性のあるTypes::Serialiser(::BooleanBase)やData::Boolのオブジェクトの場合はデコードで取り出したデータをそのままエンコードに回せます。

共通のブーリアンオブジェクトは欲しいけど、JSONじゃないものにまでJSON::PP::Booleanを使うのはなあ、という方はぜひ一度お試しください。

PERL_JSON_PP_USE_B環境変数

以前、YAPC::Hokkaidoの前夜祭でJSON::PPの数値判定処理を変更する実験的なパッチを受け入れたという話をしました。このパッチはPerl 5.27.1で一時的に(Perl側の変更のせいで)動作しなくなるなど、いささか微妙な性質のものなのですが、この変更によってJSONの出力が完全に一致するか確認するテストが壊れてしまうという苦情が届いていました。だいぶ今更ではあるのですが、状況を改善するため、JSON::PP 4.0ではPERL_JSON_PP_USE_B環境変数が真の場合は旧来のBモジュールを使った数値判定を行うようにしてあります。

Cpanel::JSON::XS由来の警告を無効に

Cpanel::JSON::XSが読み込まれた状態でJSON::PP(::Boolean)を読み込むと長らく警告が出るようになっていた(Cpanel::JSON::XS側はその警告を殺してJSON::PP::Booleanの挙動を差し替えていた)のですが、いい加減うっとおしくなっていたのでJSON::PP側でも警告を殺すようにしました。ロード順でJSON::PP::Booleanの挙動が変わるのはあまりうれしいことではないのですが、ふつうに真偽値としてのみ使ってくださる分には影響ないはずです。

番外

同じバージョン4.0つながりということでもうひとつ。今回JSONのテストを修正していてハマったことのひとつに、Cpanel::JSON::XS 3.99_01で導入されたCpanel::JSON::XS::TypeによってCpanel::JSON::XSのencode_jsonのプロトタイプが変わり、以下のようなテストが死ぬようになったことがあげられます。

ok ('[5]' eq encode_json $j1, "cjson1");

encode_json($j1)のように明示的にかっこでくくればよいだけの話ではあるのですが、ふだんからプロトタイプを利用してencode_jsonのかっこを省略している方は頭の片隅に入れておいていただければ。


というわけで、いろいろ大変なJSONまわりですが、今回4.0がらみで変わったところをまとめておきました。APIサーバなどでいずれかのモジュールをご利用の方も多いと思いますので、みなさまの方でも必要に応じて適宜ご対応いただければさいわいです。



明日は yukikimoto さんです。