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
で分析した場合分けを表現するとかもやりましたが、それはまた別で。