sjsonの使い方
ScalaでJSONを扱うためのライブラリ、sjsonについて書きます。
そもそもScalaでは標準ライブラリにJSONのパーサーがついてて、JSON文字列をパースしてcase classに入れてくれるくらいのことはしてくれます。ただし、
- 返り値の型が Option[List[Any] ] だったり
- パース結果のJSONObject(case class)をtoStringしてもJSONにならなかったり
なんで標準ライブラリに存在しているのか不明ですが、JSONをバリバリアプリケーション内で使うための機能はそろってないです。(コップ本に載ってるサンプルまんまな感じです)
JSON.scala in scala/tags/R_2_8_0_final/src/library/scala/util/parsing/json – Scala
あとは、Javaのライブラリがいろいろあるのでそれを使ってもいいんですが、当然のことながらインターフェイスがJavaっぽいので、ScalaのコレクションオブジェクトやXMLリテラルを渡してウマー、ということができません。
implicit defとかラッパーを書けばいいんですが、そこまでしなくてもScala用のJSONライブラリは他にもありますよ、というわけでsjsonです。
sbtとかmavenのリポジトリ設定
sjsonのパッケージはscala-tools.orgのMavenリポジトリに入っているので、sbtの場合はリポジトリの設定が不要です。
以下のようにプロジェクトファイルにsjsonのアーティファクトID等々を書けばOKです。
val json = "net.debasishg" %% "sjson_2.8.0" % "0.8"
Mavenを使う場合も、sjsonのライブラリはScalaランタイムと同じリポジトリにあるので、Scalaランタイムへのdependencyが書けていれば、あとはsjsonへのdependencyを追加するだけでOKです。
<repositories> <repository> <id>scala-tools.org</id> <url>http://scala-tools.org/repo-releases</url> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>scala-tools.org</id> <url>http://scala-tools.org/repo-releases</url> </pluginRepository> </pluginRepositories> <dependencies> <dependency> <groupId>org.scala-lang</groupId> <artifactId>scala-library</artifactId> <version>2.8.0.final</version> </dependency> <dependency> <groupId>net.debasishg</groupId> <artifactId>sjson_2.8.0</artifactId> <version>0.8</version> </dependency> </dependencies>
オブジェクト=>JSONへのシリアライズ
scala> import sjson.json.JsonSerialization import sjson.json.JsonSerialization scala> JsonSerialization.tojson(Map("a" -> "apple", "b" -> "Borland", "c" -> "Citrix")) res0: dispatch.json.JsValue = {"a" : "apple", "b" : "Borland", "c" : "Citrix"}
こんな感じで、Map, List, Tuple2〜22くらいならデフォルトで対応しています。
対応していない型を入れると、例外を出します。
scala> case class Neko(name : String, color : String, age : Int) defined class Neko scala> JsonSerialization.tojson(Neko("tama", "white", 666)) <<console>:34: error: could not find implicit value for parameter tjs: sjson.json.Writes[Neko] JsonSerialization.tojson(Neko("tama", "white", 666)) ^
新たな型に対応させるためには、Protocolを書く必要があります。
scala> object NekoProtocol extends DefaultProtocol { | implicit val NekoFormat : Format[Neko] = | asProduct3("name", "color", "age")(Neko)(Neko.unapply(_).get) | } defined module NekoProtocol scala> import NekoProtocol._ import NekoProtocol._ scala> JsonSerialization.tojson(Neko("tama", "white", 666)) res0: dispatch.json.JsValue = {"name" : "tama", "color" : "white", "age" : 666}
このへんの詳細については作者のブログエントリが訳されているので、そちらを参照すると良いと思います。
JSON=>オブジェクト
さっきの逆ですが、以下のとおりです。
scala> import sjson.json.JsonSerialization import sjson.json.JsonSerialization scala> JsonSerialization.tojson(Neko("tama", "white", 666)) res0: dispatch.json.JsValue = {"name" : "tama", "color" : "white", "age" : 666} scala> JsonSerialization.fromjson[Neko](res2) res1: Neko = Neko(tama,white,666)
実装
このsjsonは、いろいろと面白い実装になっていて、任意の型に対してJSONへシリアライズするためのプロトコルを定義できたり、そのプロトコルの指定をimplicit parameterでやらせてたりで、コードを読むと勉強になります。
あと sjson.json.Generic.scala を見ると、謎のマクロ的なものがあるんですが、なんだこれどうなってるんだ、という感じです。
<#list 2..9 as i> <#assign typeParams><#list 1..i as j>T${j}<#if i !=j>,</#if></#list></#assign> def asProduct${i}[S, ${typeParams}](<#list 1..i as j>f${j}: String<#if i != j>,</#if></#list>)(apply : (${typeParams}) => S)(unapply : S => Product${i}[${typeParams}])(implicit <#list 1..i as j>bin${j}: Format[T${j}]<#if i != j>,</#if></#list>) = new Format[S]{ def writes(s: S) = { val product = unapply(s) JsObject( List( <#list 1..i as j> (tojson(f${j}).asInstanceOf[JsString], tojson(product._${j}))<#if i != j>,</#if> </#list> )) } def reads(js: JsValue) = js match { case JsObject(m) => // m is the Map apply( <#list 1..i as j> fromjson[T${j}](m(JsString(f${j})))<#if i != j>,</#if> </#list> ) case _ => throw new RuntimeException("object expected") } } </#list>
どうやら、JavaのFreeMarkerというテンプレートエンジンをソースコードに適用してプリプロセッサしちゃうらしいです。
FMPP: Text file preprocessor (HTML preprocessor)
こいつを呼び出すコードがsbtのプロジェクトファイルに書かれたりしてます。
// 〜 project / build / TemplateProject.scala から抜粋 〜 // declares fmpp as a managed dependency. By declaring it in the private 'fmpp' configuration, it doesn't get published val fmppDep = "net.sourceforge.fmpp" % "fmpp" % "0.9.13" % "fmpp" val fmppConf = config("fmpp") hide def fmppClasspath = configurationClasspath(fmppConf) // creates a task that invokes fmpp def fmppTask(args: => List[String], output: => Path, srcRoot: => Path, sources: PathFinder) = { runTask(Some("fmpp.tools.CommandLine"), fmppClasspath, "-U" :: "all" :: "-S" :: srcRoot.absolutePath :: "-O" :: output.absolutePath :: args ::: sources.getPaths.toList) }
他のJSONライブラリ
sjson以外にもTwitterのやつとか、liftのやつとか、Akkaのやつがあったりします。