scalaでjsonをパース後のASTを扱いやすいように型変換する方法について調べてみた

以前の作業scalaJSONをパースできるようになりましたが、そのままの抽象構文木(AST)の状態だと扱いづらいので変換する方法について調べてみます。

調べたところspray-json-shapelessというものがあり、これが丁度やりたいことに近いかと思います。

spray-json-shapelessでは以下のようにパース後の結果に対してconvertTo[T]を呼び出すことで任意のオブジェクトに変換できているようです。

package example

object TrySprayShapeless extends App {

  import spray.json._
  import fommil.sjs.FamilyFormats._

  case class Foo(a: String, b: Double, c: Boolean)

  val sprayJsonConvertResult = """{"a":"foo","b":42.0,"c":true}""".parseJson.convertTo[Foo]
  println(sprayJsonConvertResult)

}

spray-json-shapelessはshapelessというscalaジェネリックプログラミングを可能にするライブラリを使用しているのでここまでできると思うのですが、まずはspray-json自身で基本的な型に変換するまでの処理を追えるようになりたいと思います。

spray-jsonでのパース後の型変換について

spray-json自身で基本的な型に変換するまでの処理を追えるになるですが、今回は文字列をOption[String]型に変換するところのみを試してみます。 やりたいこととしては、以下のようになります。

package example

import example.spray.json.{DefaultJsonProtocol, JsNull, JsString}

object StudySprayJson extends App {

  object a extends DefaultJsonProtocol {
    def test() = {
      println(JsString("Hello").convertTo[Option[String]] equals Some("Hello"))
    }
  }
  a.test
}

パッケージがexampleになっていますが、これはspray-jsonから必要な部分のみ抽出したかったので一部のみコピペしてexampleパッケージ配下に配置するようにしているためです。

ASTからの返還処理を読み込むのにまずはJsonReaderを見てみます。

package example.spray.json

trait JsonReader[T] {
  def read(json: JsValue): T
}

trait JsonFormat[T] extends JsonReader[T]

JsonReaderは型パラメータTを受け取り、JsValue型をT型に変換するreadメソッドが実装されるようです。JsValueはASTを表す型なのでここから変換できるようにします。JsonReaderを拡張したJsonFormatがありますが、これはもともとspray-jsonにあったJsonWriterの方もまとめるのに使っていたのですが今回はJsonWriterは必要ないのであまり気にしないで大丈夫かと思います。

jsonReaderを参照方法についてですが、パッケージオブジェクトを用意して暗黙のパラメータとして参照できるようにしています。

package example.spray

package object json {
  def deserializationError(msg: String, cause: Throwable = null, fieldNames: List[String] = Nil) = throw new DeserializationException(msg, cause, fieldNames)
  def jsonReader[T](implicit reader: JsonReader[T]) = reader
}

package json {
  case class DeserializationException(msg: String, cause: Throwable = null, fieldNames: List[String] = Nil) extends RuntimeException(msg, cause)
  class SerializationException(msg: String) extends RuntimeException(msg)
}

JsonReaderの呼び出し部分は、ASTとして情報を保持する型から呼び出すようにしています。

package example.spray.json

sealed abstract class JsValue {
  def convertTo[T :JsonReader]: T = jsonReader[T].read(this)
}

case class JsString(value: String) extends JsValue
object JsString {
  val empty = JsString("")
  def apply(value: Symbol) = new JsString(value.name)
}

case object JsNull extends JsValue

JsValue型のconvertToメソッドでjsonReaderを利用しています、convertToの型パラメータが[T :JsonReader]となっていますが、これはTを型パラメーターに受け取っているJsonReaderがスコープ内に存在したらconvertToが呼び出せるようになります。

JsStringをOption[String]に変換するためには、JsonReader[Option[T]]とJsonReader[String]を暗黙的に参照できればよさそうです。

そのために、以下のBasicFormatsとStandardFormatsを用意します。

package example.spray.json

trait BasicFormats {

  implicit object StringJsonFormat extends JsonFormat[String] {
    def read(value: JsValue) = value match {
      case JsString(x) => x
      case x => deserializationError("Expected String as JsString, but got " + x)
    }
  }
}
package example.spray.json

trait StandardFormats {

  private[json] type JF[T] = JsonFormat[T] // simple alias for reduced verbosity

  implicit def optionFormat[T :JF]: JF[Option[T]] = new OptionFormat[T]

  class OptionFormat[T :JF] extends JF[Option[T]] {
    def read(value: JsValue) = value match {
      case JsNull => None
      case x => Some(x.convertTo[T])
    }
    // allows reading the JSON as a Some (useful in container formats)
    def readSome(value: JsValue) = Some(value.convertTo[T])
  }
}

BasicFormatsではJsStringをString型に変換します。StandardFormatsはOption[T]型に変換します。この2つのフォーマットの参照方ですが、以下のようにDefaultJsonProtocolで2つのフォーマットをミックスインしておき、それをさらにミックスインしておけばconvertTOでJsStringからOption[String]に変換できます。

package example.spray.json

trait DefaultJsonProtocol
  extends BasicFormats
    with StandardFormats

object DefaultJsonProtocol extends DefaultJsonProtocol

以下のように利用できます。

package example

import example.spray.json.{DefaultJsonProtocol, JsNull, JsString}

object StudySprayJson extends App {

  object a extends DefaultJsonProtocol {
    def test() = {
      println(JsString("Hello").convertTo[Option[String]] equals Some("Hello"))
    }
  }
  a.test
}

convertToで暗黙的に渡しているreaderが正しく動いているようだったので、spray-jsonの処理を追ってみて面白かったと思いました。 spray-jsonの処理を追ったことでASTから基本的な型に変換するまではどうにかなりそうな気がします。ただ任意のがたまで行くとshapelessが必要そうなので敷居が高そう。