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}

このへんの詳細については作者のブログエントリが訳されているので、そちらを参照すると良いと思います。

sjson: Scala の型クラスによる JSON シリアライゼーション | eed3si9n

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のやつがあったりします。