Scala
datetime
json4s

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


2019/01/15 追記

json4s 3.6以降の場合はCustomSerializerを使用せず日付変換できますので、だいぶ簡単になりました。例はこちら

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


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

import java.time.ZonedDateTime

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

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

{

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

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

class CustomSerializer[A: Manifest](

ser: Formats => (PartialFunction[JValue, A], PartialFunction[Any, JValue])) extends Serializer[A]

定義だけだと分かりづらいですが、serはFormatを引数にとり、(deserialize関数, serialize関数)という形式を返す関数です。

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

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などを処理の間に挟まないといけず、もっと良い書き方あったら知りたいです。

では使ってみます。

  val jsonString =

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

val json: JValue = parse(jsonString)

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

  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を使用せず日付変換できますので、だいぶ簡単になりました。

  implicit val formats = DefaultFormats ++ JavaTimeSerializers.all

val post = json.extract[Post]


まとめ

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