コンストラクタの引数がひとつしかないcase classをJSONに変換するとき、{"key": "value"}
という形ではなく、ストレートに "value"
という形に整形したい。
spray-jsonの jsonFormat1
関数を使うと前者になってしまう。次の例は、苗字と名前で氏名を構成する概念的な統一体(conceptual whole)についてのコードだ。このコードでは苗字と名前それぞれを jsonFormat1
を用いてJSONを変換している。
case class FirstName(firstName: String)
case class LastName(lastName: String)
case class Name(first: FirstName, last: LastName)
import spray.json.DefaultJsonProtocol
object NameJsonProtocol extends DefaultJsonProtocol {
implicit val FirstNameFormat = jsonFormat1(FirstName)
implicit val LastNameFormat = jsonFormat1(LastName)
implicit val NameFormat = jsonFormat2(Name)
}
import spray.json._
import NameJsonProtocol._
println(Name(FirstName("Alice"), LastName("Brown")).toJson)
出力結果が冗長的になってしまう。firstやlastが2度出てくるためバイト数が増える。
加えて、ネストしているのでJSONをデコードするクライアント実装にも負担になる。
{"first":{"firstName":"Alice"},"last":{"lastName":"Brown"}}
理想的にはネストがなく冗長的でないJSONになってほしい。たとえばこのような:
{"first":"Alice","last":"Brown"}
これを実現するコードはこうなる:
case class FirstName(firstName: String)
case class LastName(lastName: String)
case class Name(first: FirstName, last: LastName)
import spray.json.{ JsString, JsValue, RootJsonFormat, DefaultJsonProtocol }
object NameJsonProtocol extends DefaultJsonProtocol {
// map case classes with 1 string parameter to JSON string
def jsonString[A](construct: (String) => A)(stringify: A => String): RootJsonFormat[A] = new RootJsonFormat[A] {
def write(x: A): JsString = JsString(stringify(x))
def read(json: JsValue): A = json match {
case JsString(x) => construct(x)
case x => deserializationError("Expected JsString, but got " + x)
}
}
implicit val FirstNameFormat = jsonString(FirstName)(_.firstName)
implicit val LastNameFormat = jsonString(LastName)(_.lastName)
implicit val NameFormat = jsonFormat2(Name)
}
import spray.json._
import NameJsonProtocol._
println(Name(FirstName("Alice"), LastName("Brown")).toJson)