ソモサン

私rohkiによる活動や読書の記録をつらつらと書くページです

Scala spray-json の書き分けパターンのメモ

なんか、あとどれだけ jsonリアライザを書けばいいんだ…ってぐらい書いて知見がまとまったのでメモ。
といっても大体 本家のReadme 書いてあります。

サンプルコードは割と雰囲気で書いているのでコンパイルはすぐできないかも。

素朴に

case class User(name: String, address: String)

trait ApplicationJsonFormats extends DefaultJsonProtocol {
  implicit val UserFormatter: RootJsonFormat[User] =
    jsonFormat(User,
      "name",
      "address"
    )
}

ごくごく普通の case class であれば、jsonFormat でさらっと。

コンパニオンオブジェクトがでてくる

case class User(name: String, address: String)

case class SubmitRequest(name: String, address: String)

object User {
  def apply(input: SubmitRequest): User = new User(input.name, input.address)
}

trait ApplicationJsonFormats extends DefaultJsonProtocol with SprayJsonSupport {
  implicit val UserFormatter: RootJsonFormat[User] =
    jsonFormat(User.apply,
      "name",
      "address"
    )
}

コンパニオンオブジェクトを作って apply を持たせたりすると、jsonFormat の第1引数の解決ができなくなるので、明示的に指定します。
この辺りは jsonFormat の第1引数を見るともっと別の使い方もできそうですけども、けったいなことをすると方々に怒られるので自重。

case class User(name: String, address: String)

case class SubmitRequest(name: String, address: String)

object User {
  def apply(input: SubmitRequest): User = new User(input.name, input.address)
}

trait ApplicationJsonFormats extends DefaultJsonProtocol {
  implicit val UserFormatter: RootJsonFormat[User] =
    jsonFormat[String, String, User](User.apply,
      "name",
      "address"
    )
}

さらに引数の数が同数の場合もあったりするので、解決の補助のために型情報を与えます。

Generics とか出てくる

case class User(name: String, address: String, groupId: Option[String])

case class SubmitRequest(name: String, address: String)

object User {
  def apply(input: SubmitRequest): User = new User(input.name, input.address, None)
}

case class GroupData[T](id: String, data: T)

trait ApplicationJsonFormats extends DefaultJsonProtocol {

  implicit val UserFormatter: RootJsonFormat[User] =
    jsonFormat[String, String, Option[String], User](User.apply,
      "name",
      "address",
      "group_id"
    )

  implicit def GroupDataFormatter[T: JsonFormat]: RootJsonFormat[GroupData[T]] =
    jsonFormat(GroupData[T],
      "id",
      "data"
    )
}

なんかグループとかメタデータを含んだ伝播とかで Generics がでてきたときは、Context bounds を利用します。これは implicit + 型クラスをつかう場合の糖衣構文ですが、詳しく説明できないので以下略。
Generics とかで使う場合はそのクラスに対応した JsonFormat を定義すれば OK です。ただし順序に気を付けて 入れ子になる方を先に宣言する必要があります。

表面のシンタックスを普通の型パラメータと区別できるようにしてほしい気持ちが若干あったり…ぱっとみてわからないのは、ちとつらいです。

さらーに型パラメータとかでてくる

trait GroupItem {
  def groupId: Option[String]
}

case class User(name: String, address: String, groupId: Option[String]) extends GroupItem

case class SubmitRequest(name: String, address: String)

object User {
  def apply(input: SubmitRequest): User = new User(input.name, input.address, None)
}

case class GroupData[T <: GroupItem](id: String, data: T)

trait ApplicationJsonFormats extends DefaultJsonProtocol {
  implicit val UserFormatter: RootJsonFormat[User] =
    jsonFormat[String, String, Option[String], User](User.apply,
      "name",
      "address",
      "group_id"
    )

  implicit def GroupDataFormatter[T <: GroupItem : JsonFormat]: RootJsonFormat[GroupData[T]] =
    jsonFormat(GroupData[T],
      "id",
      "data"
    )
}

なんか同じように取り扱いたいケースが出てきて、trait にくくりだすとかです。

ここまでくるとなんか見直した方がいいんじゃあない? って気持ちがふつふつとわいたりわかなかったり。
情報はみんな大好き stackoverflow から。
[T <: GroupItem : JsonFormat] がぱっとわからなかったんですよねー。 手前が T の型制約、後ろが型クラスの実装指定…のはず。

再帰構造

case class Group(id: String, name: String, subGroup: Array[Group])

trait ApplicationJsonFormats extends DefaultJsonProtocol {
  implicit val GroupFormatter: RootJsonFormat[Group] = 
    lazyFormat(jsonFormat(Group,
      "id",
      "name",
      "sub_group"
    ))
}

Group なんて出てくると、まぁ再帰構造になるわけで。
先ほどもあった通り、本来は入れ子になるほうは先に宣言してやる必要があります。
自分自身が宣言できていない以上、順番も何もないので lazyFormat で後回ししてやるかんじです。
こいつの中はまだみれてないので、全然仕組みがわかってないです。どうやってるんだろ。

なやみ

trait + Generics ときにどう解決させるかが悩みどころ。
trait のフォーマッターを作ってもいいけど、型にした意味がないし。だからと言っていちいち型を指定させるのも本末転倒。うーむ。 方針のとっかかりはできているんですが、形にできず。

おわり

こんなんばっか書いてますが、Scala の型クラスとか型パラメータを結構勉強できたので結果おーらいかなー。
trait で分析した場合分けを表現するとかもやりましたが、それはまた別で。