調べたことを雑に残すシリーズです。
Spray-Jsonの基本的な使い方は以下の通りです。
case class Color(name: String, red: Int, green: Int, blue: Int) object MyJsonProtocol extends DefaultJsonProtocol { implicit val colorFormat = jsonFormat4(Color) } import MyJsonProtocol._ import spray.json._ val json = Color("CadetBlue", 95, 158, 160).toJson val color = json.convertTo[Color]
- case classであれば個別に自分で変換のためのコードを書く必要は無い
- ただし、case classのフィールドの個数を、
jsonFormatN
というメソッドで指定する
フィールドの個数だけは指定させているのはなぜでしょう?
jsonFormatNメソッド
jsonFormatNメソッドは以下のソースコードで定義されています。
ただし、これはテンプレートであり、実際のコードはsbt-boilerplate
というpluginで生成されるようになっています。Scalaでおなじみの引数が1〜22個までの同じ挙動のメソッドを自動で生成するための仕組みになっています。
一度コンパイルすると、以下の場所にソースが生成されます。
spray-json/target/scala-2.12/src_managed/main/spray/json/ProductFormatsInstances.scala
例えば引数を二つ取るjsonFormat2
は以下のようなコードに展開されます。
// Case classes with 2 parameters def jsonFormat2[P1 :JF, P2 :JF, T <: Product :ClassTag](construct: (P1, P2) => T): RootJsonFormat[T] = { val Array(p1, p2) = extractFieldNames(classTag[T]) jsonFormat(construct, p1, p2) } def jsonFormat[P1 :JF, P2 :JF, T <: Product](construct: (P1, P2) => T, fieldName1: String, fieldName2: String): RootJsonFormat[T] = new RootJsonFormat[T]{ def write(p: T) = { val fields = new collection.mutable.ListBuffer[(String, JsValue)] fields.sizeHint(2 * 3) fields ++= productElement2Field[P1](fieldName1, p, 0) fields ++= productElement2Field[P2](fieldName2, p, 1) JsObject(fields.toSeq: _*) } def read(value: JsValue) = { val p1V = fromField[P1](value, fieldName1) val p2V = fromField[P2](value, fieldName2) construct(p1V, p2V) } }
先ほどの implicit val colorFormat = jsonFormat4(Color)
は、オブジェクトを渡していますが、以下の仕組みにより、実際にはColor.applyが渡されているのと同等です。
つまり、そのcase classのファクトリメソッドが渡されているわけです。
write
はJSON astを生成するメソッドなので、ここで必要になってくるのはread
の方です。
JSON astからScalaオブジェクトを生成する
先ほどのreadメソッドの中を一つ一つ見ていきましょう。
二つのフィールドをJSON astが格納されているvalue
からfromField
メソッドを使って取り出しているようですね。続いてfromField
メソッドの中身を見ていきましょう。
fromField
メソッドは、src/main/scala/spray/json/ProductFormats.scala
で以下のように定義されています。
protected def fromField[T](value: JsValue, fieldName: String) (implicit reader: JsonReader[T]) = value match { case x: JsObject if (reader.isInstanceOf[OptionFormat[_]] & !x.fields.contains(fieldName)) => None.asInstanceOf[T] case x: JsObject => try reader.read(x.fields(fieldName)) catch { case e: NoSuchElementException => deserializationError("Object is missing required member '" + fieldName + "'", e, fieldName :: Nil) case DeserializationException(msg, cause, fieldNames) => deserializationError(msg, cause, fieldName :: fieldNames) } case _ => deserializationError("Object expected in field '" + fieldName + "'", fieldNames = fieldName :: Nil) }
ちょっと複雑で分かりづらいですが…
- 一つ目は、そのフィールドがオプションでかつ値を持たなければNoneを返しています
- 二つ目は、JsObjectから指定されたフィールド名で値を取り出しています。
- そのどちらにも当てはまらない場合は、例外発生です。
元々、case classの引数を取得しているはずなので、JsObjectとして値が取れないと例外なのは妥当ですね。
read
は取り出す値の型ごとに用意されていて、例えばStringとして値を取得したい場合は以下のメソッドが呼ばれます。
implicit object StringJsonFormat extends JsonFormat[String] { def write(x: String) = { require(x ne null) JsString(x) } def read(value: JsValue) = value match { case JsString(x) => x case x => deserializationError("Expected String as JsString, but got " + x) } }
JsStringであれば、値が取り出され、それ以外は例外になることが分かります。
基本的な型については、src/main/scala/spray/json/BasicFormats.scala
で定義されています。
こうやって値を一つ一つ生成していって、最終的にはScalaのcase classからのオブジェクトがapplyメソッド(相当)で生成されます。
ランタイムリフレクションを使って、メソッドの引数と型を一つ一つ特定する方法もありますが、こちらの方がシンプルですね。
ということで、Spray-JSONが引数の個数を指定させるのは、case classのオブジェクトが型情報を引き回してくれることを利用して型を特定する方法を使うため、ということが分かりました。