Json4sのCustomSerializerを使ってJsonの日付をZonedDateTimeにしたい

# 目的
java8から導入された各種日付型が便利です。

不変でスレッドセーフ、かつDateTimeFormatterと組み合わせて日付文字列をうまい具合に変換も可能です。

今回Json4sでJsonを扱っていますが、jsonの日付文字列もjava8の日付型に変換して扱いたいです。
公式をみると、java.util.Dateへの変換は容易にできそうですが、それ以外は自分でCustomSerializerを定義する必要がありそうです。

参考： [json4s/json4s: A single AST to be used by other scala json libraries](https://github.com/json4s/json4s)

## 2019/01/15 追記
json4s 3.6以降の場合はCustomSerializerを使用せず日付変換できますので、だいぶ簡単になりました。例は[こちら](#20190115-追記-1)

今回は例として、Jsonの文字列をZonedDateTimeを含むCase Classへ変換します。

# 例
例として、記事コンテンツを表すPostクラスを定義してみます。

```scala
import java.time.ZonedDateTime

case class Post(title: String, content: String, dateTime: ZonedDateTime)
```

新しい記事のjsonが投稿されたとします。これをPostへ変換します。変換には、CustomSerializerを継承したPostSerializerを定義すればOkです。

```json
{
  "title": "Json4sの使い方",
  "content": "Json4sの使い方は...このようになってます",
  "datetime": "2019-01-12T03:15:00Z"
  }
```

CustomSerializerの定義をまず見てみます。

```scala
class CustomSerializer[A: Manifest](
  ser: Formats => (PartialFunction[JValue, A], PartialFunction[Any, JValue])) extends Serializer[A]
```
定義だけだと分かりづらいですが、serはFormatを引数にとり、`(deserialize関数, serialize関数)`という形式を返す関数です。


PostSerializerを定義してみます。複雑な書き方で申し訳ないですが、引数の型が特殊なのでいい書き方が思いつかない。。

```scala
import java.time.format.DateTimeFormatter
import java.time.{Instant, ZoneId, ZonedDateTime}

import org.json4s.CustomSerializer
import org.json4s.JsonAST.JObject
import org.json4s.JsonDSL._

class PostSerializer extends CustomSerializer[Post](format => (
// deserialze関数
{
  case jObject: JObject =>
    implicit val fmt = format

    val title = (jObject \ "title").extract[String]
    val content = (jObject \ "content").extract[String]
    val datetimeStr = (jObject \ "datetime").extract[String]
    val datetime = PostSerializer.parseToZonedDateTime(datetimeStr)

    Post(title, content, datetime)
},
// serialze関数
{
  case rssItem: Post =>
    ("title" -> rssItem.title) ~
      ("content" -> rssItem.content) ~
      ("datetime" -> PostSerializer.strFromZonedDateTime(rssItem.dateTime))
}
))

object PostSerializer {
  private def strFromZonedDateTime(t: ZonedDateTime): String =
    t.format(DateTimeFormatter.ISO_INSTANT)

  private def parseToZonedDateTime(t: String): ZonedDateTime = {
    Instant.parse(t).atZone(ZoneId.systemDefault())
  }
}
```

serialize関数内では、`~`というJValueのマージ関数を使っています。一見わかりにくいです。使用するには`import org.json4s.JsonDSL._`を宣言します。

PostSerializerはコンパニオンオブジェクトも定義しています。メソッドでZonedDateTimeとStringの相互変換を行います。
ZonedDateTimeは便利とはいえ、stringから変換するにはInstantなどを処理の間に挟まないといけず、もっと良い書き方あったら知りたいです。

では使ってみます。

```scala
  val jsonString =
    """
      |{
      |  "title": "Json4sの使い方",
      |  "content": "Json4sの使い方は...このようになってます",
      |  "datetime": "2019-01-12T03:15:00Z"
      |}
    """.stripMargin

  val json: JValue = parse(jsonString)
```

記事投稿のjsonから`extract`で変換します。

```scala
  implicit val formats = DefaultFormats + new PostSerializer()
  val post = json.extract[Post]

  println(post) // =>Post(Json4sの使い方,Json4sの使い方は...このようになってます,2019-01-12T12:15+09:00[Asia/Tokyo])

```

注意ですが、extractを使用したい場合formatsをimplicitで宣言する必要があります。

## 2019/01/15 追記
json4s 3.6以降の場合は下記のようにするとCustomSerializerを使用せず日付変換できますので、だいぶ簡単になりました。

```scala
  implicit val formats = DefaultFormats ++ JavaTimeSerializers.all

  val post = json.extract[Post]
```

# まとめ
JsonからZonedDatetimeは単純にextractで抽出できなく、CustomSerialzerを定義してがんばります。以外に記述量多くなりました。もしかしたらcirceなどの他ライブラリだと短くなるかもしれないですね。