読者です 読者をやめる 読者になる 読者になる

sjsonを使った場合に継承したらtojson・fromjsonできるようにしたかった話

sjonつかってますか

みなさんこんばんは。私は連休が終わってしまってとても悲しいです。
ところで、ScalaのsjsonというJSONを扱うライブラリがあるんですが、
型クラスを使って色々やってくれたりして面白くて便利です。


型クラスを使ってクラスのプロトコル実装することで、シリアライズ部分を
自前で定義できるのがSJSONの非常に素晴らしいところです。
ただ、使っていると、このような暗黙変換をたくさん定義しなければならない
なってきてだんだん面倒臭くなってきます。

object Protocols extends DefaultProtocol {
  implicit val authorFormat: Format[Author] = 
    asProduct3("firstName", "lastName", "name")(Author)(Author.unapply(_).get)

  implicit val hogeFormat: Format[Hoge] = 
    asProduct4("a", "b", "c", "d")(Hoge)(Hoge.unapply(_).get)

  implicit val fugaFormat: Format[Fuga] = 
    asProduct5("a", "b", "c", "d", "e")(Fuga)(Fuga.unapply(_).get)
}

case class Author(var firstName: String,var lastName: String,var email: String)
case class Hoge(a: String, b: Int, c: Double, d: Long)
case class Fuga(a: String, b: Int, c: Double, d: Long, e: String)


プロトコルはきちんと1つづつきちんと決めたい気持ちはわかりますが、
せめてcase classくらいはDRYに記述したいので、基底クラスを用意し、
プロトコルを1つ用意することで、派生クラス側のプロトコルを書かずに済ませようとする試みが今回の主題です。


できたコードはgithubに置いてあります。
https://github.com/voidy21/sjson_sample1

こんな感じの基底クラスを用意する

色々とひどいですが、こんな感じで基底クラスを用意しておくとよさそうです。

import sjson.json._
import DefaultProtocol._
import JsonSerialization._
import Serializer.SJSON
import dispatch.json._

object ClassUtil {
  def getFields(c: Class[_]): List[String] =
    if (c == classOf[Object]) Nil
    else getFields(c.getSuperclass) ++
      c.getDeclaredFields.map(_.getName).filter {n => try {
          c.getMethod(n)
          true
      } catch {
        case e: NoSuchMethodException => false
      }
    }.toList
}

trait BaseModel {
  def toJson = tojson(this)

  def fieldNames = ClassUtil.getFields(getClass)

  def fieldMap = fieldNames.map {f => (f, getClass.getMethod(f).invoke(this))}
}

object BaseModel {
  implicit def baseModelFormat[T <: BaseModel]
    (implicit m: ClassManifest[T]) : Format[T] = new Format[T] {

    def writes(o: T) = JsObject(
      o.fieldMap.map{case (k, v) =>
        (JsString(k), JsValue(v))
      }.toList
    )

    def reads(json: JsValue) = json match {
      case JsObject(n) => {
        val c = m.erasure
        val fields = ClassUtil.getFields(c)
        val constructor = c.getConstructors.head
        val types = constructor.getParameterTypes
        val args = (types zip fields).map{case (t, name) =>
          (SJSON.in(n(JsString(name))).asInstanceOf[Any] match {
            case num: scala.math.BigDecimal => t.toString match {
              case "int" => num.toInt.asInstanceOf[java.lang.Integer]
              case "double" => num.toDouble.asInstanceOf[java.lang.Double]
              case "long" => num.toLong.asInstanceOf[java.lang.Long]
              case _ => throw new RuntimeException("JsObject expected")
            }
            case default => default
          }).asInstanceOf[Object]}.toList
        constructor.newInstance(args:_*).asInstanceOf[T]
      }
      case _ => throw new RuntimeException("JsObject expected")
    }
  }

}

こんな感じで使う

プロトコルを定義するのではなく、基底クラスを継承させるだけでおk。
あと、モデル側からtoJson呼べるようにしておいたので、ちょっとだけRailsぽくなった

import sjson.json._
import DefaultProtocol._
import JsonSerialization._
import dispatch.json._
import BaseModel._

case class Author(var firstName: String, var lastName: String, var email: String) extends BaseModel

case class Hoge(a: String, b: Int, c: Double, d: Long) extends BaseModel

object Main extends App {
  val author = Author("tanaka", "taro", "tanaka@example.com")
  val json1 = author.toJson
  println(json1)
  println(fromjson[Author](Js("""{"firstName" : "tanaka", "lastName" : "taro", "email" : "tanaka@example.com"}""")))

  val hoge = Hoge("a", 1, 5.5, 10L)
  val json2 = hoge.toJson
  println(json2)
  println(fromjson[Hoge](Js("""{"a" : "a", "b" : 1, "c" : 5.5, "d" : 10}""")))

  val authors = List(
    Author("a", "b", "c@d.e"),
    Author("f", "g", "h@i.j"),
    Author("v", "w", "x@y.z")
  )
  val json3 = tojson(authors)
  println(json3)
  println(fromjson[List[Author]](Js("""[{"firstName" : "a", "lastName" : "b", "email" : "c@d.e"}, {"firstName" : "f", "lastName" : "g", "email" : "h@i.j"}, {"firstName" : "v", "lastName" : "w", "email" : "x@y.z"}]""")))
}
実行結果
{"firstName" : "tanaka", "lastName" : "taro", "email" : "tanaka@example.com"}
Author(tanaka,taro,tanaka@example.com)
{"a" : "a", "b" : 1, "c" : 5.5, "d" : 10}
Hoge(a,1,5.5,10)
[{"firstName" : "a", "lastName" : "b", "email" : "c@d.e"}, {"firstName" : "f", "lastName" : "g", "email" : "h@i.j"}, {"firstName" : "v", "lastName" : "w", "email" : "x@y.z"}]
List(Author(a,b,c@d.e), Author(f,g,h@i.j), Author(v,w,x@y.z))


テキトーに作っているので実はShort型とかを使ったりすると落ちたりするが、
Int、Double, Stringあたりを使った基本的なcase classについてはよさげな感じ。