はじめに
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 ライブラリ内のコードがこちら↓
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 するようにします。
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#registerTypeAdapter
で Date
と MyDateTypeAdapter
を関連づけます。
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
を持っています。
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 で意図しないデータの変化が発生しないか気をつけた方が良い