Windows環境でjacksonのObjectMapperでObjectをデコードする際の問題

897
NO IMAGE

問題点

com.fasterxml.jackson.databind.ObjectMapperを使い、InstantBigDecimalなどのjacksonで変換されると浮動小数点数になる値を持つObjectをデコードした際に、エンコード前の値にならない場合がある。

発生する環境

Windows環境全般
※確認できている限り、他の環境では発生しない。

原因

 「値がおかしくなることがある実装例」のように、InstantObjectのようなInstantを持つObjectをデコードする際、一旦JsonNodeへの変換すると、一旦doubleに変換され、その後、Instantに変換される。
 この時、windowsだとdobuleの精度がデフォルトで53bitしかないため値がおかしくなることがある。
 「直接変換を行うため、値がおかしくならない実装例」のように、途中でJasonNodeへ変換せず、直接変換する場合は問題ない。

  • 変換対象

    public class InstantObject {
    
        // この値がおかしくなる
        private Instant _time;
    
        public InstantObject() {
        }
    
        public Instant time() {
            return _time;
        }
    
        public void time(Instant time) {
            this._time = time;
        }
    
    }
  • 「値がおかしくなることがある実装例」

    ObjectMapper om = new ObjectMapper();
    JavaTimeModule jtm = new JavaTimeModule();
    om.registerModule(jtm);
    SimpleModule module = new SimpleModule();
    // InstantObjectのデシリアライザー
    module.addDeserializer(InstantObject.class, new JsonDeserializer<InstantObject>() {
    
        // デコード処理
        @Override
        public InstantObject deserialize(JsonParser p, DeserializationContext ctxt)
                throws IOException, JsonProcessingException {
            InstantObject ret = new InstantObject();
            ObjectCodec codec = p.getCodec();
            // ここで、文字列→JsonNodeに変換される際に、timeの値が一旦がdoubleになり、ここでおかしくなる場合がある。
            JsonNode node = codec.readTree(p);
            JsonNode v = node.get("time");
            System.out.println("=======================");
            System.out.println("is Double? : " + v.isDouble());
            System.out.println("=======================");
            ret.setTime(p.getCodec().treeToValue(v, Instant.class));
            return ret;
        }
    
    });
    om.registerModule(module);
    
    InstantObject now = new InstantObject();
    Instant targetTime = Instant.now();
    now.setTime(targetTime);
    // エンコード:Instant→BigDecimal→文字列に変換される
    String encoded = om.writeValueAsString(now);
    try {
        // デコード:文字列→double→Instantに変換される
        InstantObject decoded = om.readValue(encoded, InstantObject.class);
        Instant decodedTime = decoded.getTime();
        // targetTimeとdecodedTimeの値が同じにならない場合がある。
    } catch (IOException e) {
        throw new RuntimeException("Failed to decode <" + encoded + ">.", e);
    }
    • 上記コードの実行ログ:デコードした際にDoubleになっている。
      =======================
      is Double? : true
      =======================
  • 「直接変換を行うため、値がおかしくならない実装例」

    ObjectMapper om = new ObjectMapper();
    JavaTimeModule jtm = new JavaTimeModule();
    om.registerModule(jtm);
    Instant in = Instant.now();
    // エンコード:Instant→BigDecimal→文字列に変換される
    String encoded = om.writeValueAsString(in);
    try {
        // デコード:文字列→Instantに変換される
        Instant decoded = om.readValue(encoded, Instant.class);
    } catch (IOException e) {
        throw new InternalException("Failed to decode <" + encoded + ">.", e);
    }

解決方法

 デコードする際、途中でJasonNodeへ変換をする場合でも、浮動小数点数をDoubleNodeではなく、DecimalNodeに変換すればいいため、以下のようにUSE_BIG_DECIMAL_FOR_FLOATSを有効にする。
 ※この設定をすることで、文字列→BigDecimalInstantに変換されるようになる

ObjectMapper om = new ObjectMapper();
om.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);