問題点
com.fasterxml.jackson.databind.ObjectMapper
を使い、Instant
やBigDecimal
などの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 =======================
- 上記コードの実行ログ:デコードした際にDoubleになっている。
-
「直接変換を行うため、値がおかしくならない実装例」
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を有効にする。
※この設定をすることで、文字列→BigDecimal
→Instant
に変換されるようになる
ObjectMapper om = new ObjectMapper();
om.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);