LoginSignup
1
0

More than 3 years have passed since last update.

Gson で Date を json 化すると秒単位の精度に丸められる問題と対応

Last updated at Posted at 2019-08-28

はじめに

Gson とは、Google 製のライブラリで、Java(Kotlin)のオブジェクトと json の相互変換が容易に可能になります。
しかし、Gson のデフォルトのままで、 java.util.Date を json 化すると、ミリ秒の情報が落ち、秒単位に丸められていました。

Gson の使い所

本を表す Java(Kotlin) オブジェクトを

data class Book(val name: String, val price: Int)

val gson = Gson()
val book = Book("ABC", 123)
val json = gson.toJson(book) // {"name":"ABC","price":123}
val restoredBook = gson.fromJson(json, Book::class.java)
// restoredBook == book -> true

の様に、json 化したり、json からオブジェクトに復元することが簡単に行えます。

ハマったところ

java.util.Date を持つオブジェクトを Gson で json 化したら、秒単位に丸められました。

先ほどの Book に出版日を Date 型で追加します。

data class Book(val name: String, val price: Int, val publishDate: Date)

先ほどと同様に json 化すると、

{"name":"ABC","price":123,"publishDate":"Aug 28, 2019 11:25:30 PM"}

追加した publishDate も json のプロパティに出力されています。
しかしながら、 元の Date が持っていたミリ秒の情報が欠落して json に出力されています。

具体的な値を記すと、

val gson = Gson()
val book = Book("ABC", 123, Date()) // Book(name=ABC, price=123, publishDate=Wed Aug 28 23:25:30 GMT+09:00 2019)
val time1 = book.time // 1567002330140
val json = gson.toJson(book) // {"name":"ABC","price":123,"publishDate":"Aug 28, 2019 11:25:30 PM"}
val restoredBook = gson.fromJson(json, Book::class.java) // Book(name=ABC, price=123, publishDate=Wed Aug 28 23:25:30 GMT+09:00 2019)
val time2 = restoredBook.time // 1567002330000
// restoredBook == book -> false

となります。
肝となる差分は、

time1 // 1567002330140
time2 // 1567002330000

です。
そのため、

assertEquals(book, restoredBook)

のような Unit test は失敗します。
(Unit test を書いてて、この差分に気がつきました。)

原因

Gson のデフォルトの DateTypeAdapter が、 Date を DateFormat した文字列で serialize / deserialize しているため。
Gson ライブラリ内のコードがこちら↓

DateTypeAdapter.java
public final class DateTypeAdapter extends TypeAdapter<Date> {
  ...
  private synchronized Date deserializeToDate(String json) {
    for (DateFormat dateFormat : dateFormats) {
      try {
        return dateFormat.parse(json);
      } catch (ParseException ignored) {}
    }
    try {
        return ISO8601Utils.parse(json, new ParsePosition(0));
    } catch (ParseException e) {
      throw new JsonSyntaxException(json, e);
    }
  }

  @Override public synchronized void write(JsonWriter out, Date value) throws IOException {
    if (value == null) {
      out.nullValue();
      return;
    }
    String dateFormatAsString = dateFormats.get(0).format(value);
    out.value(dateFormatAsString);
  }
  ...
}

対策

案1:Date を gson に渡さない

先ほどの

data class Book(val name: String, val price: Int, val publishDate: Date)

Date -> Long にして、

data class Book(val name: String, val price: Int, val publishDate: Long)

としてしまうのも1つの手です。
(String でも良いかもしれません。)

ただ、Long にしても String にしても、どういう形式(ミリ秒単位や秒単位など)なのかをきちんと定める必要が出てきてしまいます。。。

案2:自作の DateTypeAdapter を使う

Gson には、 TypeAdapter を登録してその型の serialize / deserialize 方法をカスタマイズすることができます。
Gson のデフォルトの DateTypeAdapter が、 Date を DateFormat した文字列で serialize / deserialize いましたが、 Date#getLong() の値で serialize / deserialize するようにします。

MyDateTypeAdapter.kt
class MyDateTypeAdapter : TypeAdapter<Date?>() {

    override fun write(out: JsonWriter?, value: Date?) {
        if (value == null) {
            out?.nullValue()
            return
        }
        out?.value(value.time) // Date#getLong() で serialize
    }

    override fun read(`in`: JsonReader?): Date? {
        if (`in`?.peek() == JsonToken.NULL) {
            `in`.nextNull()
            return null
        }
        return Date().apply { time = `in`?.nextString()?.toLong() ?: 0 } // deserialize
    }

}

を作り、 GsonBuilder#registerTypeAdapterDateMyDateTypeAdapter を関連づけます。

val gson = GsonBuilder()
    .registerTypeAdapter(Date::class.java, MyDateTypeAdapter())
    .create()

このようにすると、 Date のままでも serialize / deserialize でミリ秒までの情報がきちんと保持されるようになります。

注意

いずれの方法でも、 serialize / deserialize は問題なく行えるはずです。
しかしながら、既に Aug 28, 2019 11:25:30 PM の形式でどこか(端末ローカルなど)に serialize されている 従来形式のデータを deserialize することができなくなってしまいます。

Gson 内の DateTypeAdapter では、いくつかの DateFormat を持っています。

DateTypeAdapter.java
private synchronized Date deserializeToDate(String json) {
  for (DateFormat dateFormat : dateFormats) {
    try {
      return dateFormat.parse(json);
    } catch (ParseException ignored) {}
  }
  try {
    return ISO8601Utils.parse(json, new ParsePosition(0));
  } catch (ParseException e) {
    throw new JsonSyntaxException(json, e);
  }
}

これと同様に、いくつかの parse 方法を用意しておいて、parse に失敗したら次の方法で parse するなどの策を取っておく必要があります。

まとめ

  • Gson で、そのまま Date を json 化すると、秒単位の精度に丸められる
  • 回避方法はあるが、導入や変更にタイミングには注意が必要
  • Gson に限らず、他のライブラリでも serialize / deserialize で意図しないデータの変化が発生しないか気をつけた方が良い
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0