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についてはよさげな感じ。