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

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

## Gson の使い所
本を表す Java（Kotlin） オブジェクトを

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

```kotlin
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` 型で追加します。

```kotlin
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 に出力されています。**

具体的な値を記すと、

```kotlin
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
```
です。
そのため、

```kotlin
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 に渡さない
先ほどの

```kotlin
data class Book(val name: String, val price: Int, val publishDate: Date)
```
を `Date` -> `Long` にして、

```kotlin
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#registerTypeAdapter` で `Date` と `MyDateTypeAdapter` を関連づけます。

```kotlin
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 で意図しないデータの変化が発生しないか気をつけた方が良い