その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

jackson-databind で int の取りうる値を超えた場合の挙動

Javaの話題です.

jackson-databind を使って JSON のデシリアライズを行っていて,数値を int にマッピングしている場合,その値が int (32bit) の取りうる値を超えた時の挙動が「バージョンによって異なって」います.
以下に挙げる挙動は 2.9.3 から 2.9.4 へのアップグレードで変更されています.


jackson-databind-2.9.3 を利用している場合,値が int の範囲を超過するとその値を int にキャストしたもの *1 にデシリアライズされます.

コード:


一方,jackson-databind-2.9.4 で同様のシチュエーションになった時にどうなるかというと,このバージョンからは例外が上げられることになります.

コード:

新たに導入された _convertNumberToInt というメソッドの内部で値が int の取りうる範囲内かどうかを判断し,範囲内でない場合には reportOverflowInt で例外を送出するようになっています.


この変更がどこで入ったのかというと,どうやらこのコミットのようです: Fix #1729 · FasterXML/jackson-databind@6a1152c · GitHub

このコミットログに記されている #1729 がどのようなレポートかと言うと

github.com

「int が範囲外だった時,従来の実装では意図しない値になってしまうので明示的になるように例外を上げてほしい.jackson-core の ParserBase では int が範囲外の際に例外を上げている」という内容のようです.
Author の方も「挙動に一貫性を持たせるためにも,範囲外のときには例外を上げるようにしたほうがよい」と返答をしており,これについては賛成するところです (暗黙的な wraparound は脆弱性の元にもなり得るので).


さて一方で uint32 の範囲でおさまる値 (uint32 の範囲内だと値を負に rewind しても一意になる) を取り扱っているときに,従来の挙動を意識せず利用している場合,予期せぬクラッシュが起こります.
手っ取り早く直すためには int ではなく long として取り扱うというのが良いでしょうが,既存データとの兼ね合いなどの色々な事情によってそうもゆかないこともあるでしょう.そのような際にはカスタムシリアライザ・デシリアライザを書いて乗り切ることになると思います.

import com.fasterxml.jackson.databind.util.StdConverter;

public static class Uint32JacksonSerializer extends StdConverter<Integer, Long> {
    @Override
    public Long convert(Integer n) {
        if (n == null) {
            return 0L; // as you like
        }
        return n.longValue();
    }
}
import com.fasterxml.jackson.databind.util.StdConverter;

public static class Uint32JacksonDeserializer extends StdConverter<Long, Integer> {
    private static final long MAX_UINT32_VALUE = 4294967295L;

    @Override
    public Integer convert(Long n) {
        if (n == null) {
            return 0; // as you like
        }
        if (n > MAX_UINT32_VALUE) {
            throw new IllegalArgumentException("out of the boundary of uint32 value: " + n);
        }
        return n.intValue();
    }
}

このようにカスタム (デ) シリアライザを書いて,

public class JsonClass {
    @JsonSerialize(converter = Uint32JacksonSerializer.class)
    @JsonDeserialize(converter = Uint32JacksonDeserializer.class)
    private Integer foo;
}

などとしてあげると,入れる時・出す時によしなに値を変換して取り扱ってくれるようになります.



しかしこれがパッチバージョンとして入ってくるレベルの変更かというと……大変ですね.まあリリースノートにはしっかり書いてあるのですが……jackson-databind/VERSION-2.x at ae9c91d254954a963cc525e941564c5348181eac · FasterXML/jackson-databind · GitHub

教訓としては「bit長にゆとりを持ってデータ設計をしましょう」「リリースノートをちゃんと読もう」ということです,現場からは以上です.

*1:例えば `2147483648` が来た場合に rewind されて `-2147483648` になる