Scalatra2.2+scalatra-jsonでJSONを処理する

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

scalatra-json

Scalatraでは、JSONライブラリとしてlift-jsonをベースとしたscalatra-lift-jsonが提供されているので、JSONを利用する際はそれを使っていました。しかし、Scalatra 2.2系(現在まだRCです)からscalatra-lift-jsonの提供をやめ、json4sをベースとしてscalatra-jsonという名前でJSONライブラリを提供するようです。

json4sは、Scala(もしくはJava)の各種JSONライブラリを同じように扱うこと(正確には、JSON処理のための単一のASTを提供すること)を目的としたライブラリです。ちょうど、ロギングライブラリのslf4jがlogbackやlog4jのファサードとなっているのと同じです。ScalaのJSONライブラリの中では使いやすい、lift-jsonに近い形でJSONが扱えるようになっているようです。実際に利用するパーサーは、今のところjson4sのネイティブパーサーとJacksonに対応しています。

scalatra-lift-jsonからscalatra-jsonに乗せかえてみましたので使い方をメモします。

開発環境

今回の開発環境は以下の通りです。

  • OSX Mountain Lion
  • Scala 2.9.2
  • sbt 0.12.1
  • giter8 0.4.5

Scalatraプロジェクトの作成

まずはScalatraプロジェクトを作ります。sbt+giter8を使うのが楽です。giter8はScalaアプリケーションのひな形を作成するためのツールです。giter8がインストールされていない場合は、Homebrewからインストールします。

$ brew install giter8

giter8でScalatraプロジェクトのテンプレートを作成します。

$ g8 scalatra/scalatra-sbt

今回のプロジェクトの設定は以下のようにしました。

organization [com.example]: jp.classmethod
package [com.example.myapp]: jp.classmethod.scalatrajson
name [My Scalatra Web App]: scalatra-json
servlet_name [MyServlet]: JsonTestServlet
version [0.1.0-SNAPSHOT]: 

作成したプロジェクトのディレクトリに移動して、sbtを起動します。

$ cd scalatra-json/
$ sbt

sbtが起動してreloadとupdateが済んだら、以下のコマンドでJettyを起動します。

> container:start

localhost:8080にアクセスしてウェルカムページが表示されれば、Scalatraプロジェクトの作成は成功です。

ビルド依存性の修正

scalatra-jsonが使えるように、build.sbtを以下の通りに修正します。

build.sbt

organization := "jp.classmethod"

name := "scalatra-json"

version := "0.1.0-SNAPSHOT"

scalaVersion := "2.9.2"

seq(webSettings :_*)

classpathTypes ~= (_ + "orbit")

libraryDependencies ++= Seq(
  "org.scalatra" % "scalatra" % "2.2.0-SNAPSHOT",
  "org.scalatra" % "scalatra-scalate" % "2.2.0-SNAPSHOT",
  "org.scalatra" % "scalatra-specs2" % "2.2.0-SNAPSHOT" % "test",
  "org.scalatra" % "scalatra-json" % "2.2.0-SNAPSHOT",
  // "org.json4s" %% "json4s-native" % "3.1.0", // ネイティブパーサーを利用する場合はこっち
  "org.json4s" %% "json4s-jackson" % "3.1.0",
  "ch.qos.logback" % "logback-classic" % "1.0.6" % "runtime",
  "org.eclipse.jetty" % "jetty-webapp" % "8.1.7.v20120910" % "container;test",
  "org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar"))
)

resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"

Scalatra2.2系はまだRCですので、SNAPSHOTを使います。また、今回はScala 2.9.2を使いますが、Scala 2.10系で動かす場合は以下のようにバイナリバージョンを付加するようsbtに指示する必要があります。

"org.scalatra" % "scalatra" % "2.2.0-SNAPSHOT" cross CrossVersion.binary,

サーブレットの修正

以下のエントリでscalatra-lift-jsonの使い方が紹介されていたので、サーブレットの実装を真似させてもらいました。

Scalatra+scalatra-lift-jsonでJSON

JSONパーサーはJacksonを使いました。

JsonTestServlet.scala

package jp.classmethod.scalatrajson

import org.scalatra._
import scalate.ScalateSupport
import org.json4s._
import org.scalatra.json._

class JsonTestServlet extends ScalatraServlet with ScalateSupport with JacksonJsonSupport {

  protected implicit val jsonFormats: Formats = DefaultFormats

  get("/") {
    scaml("index")
  }

  get("/json") {
    Extraction.decompose(
      Person(1, "ぱみゅぱみゅ", 19)
    )
  }

  post("/json") {
    parsedBody match {
      case JNothing => halt(400, "invalid json")
      case json: JObject => {
        val pamyu: Person = json.extract[Person]
        pamyu.name
      }
      case _ => halt(400, "unknown json")
    }
  }

  notFound {
    // remove content type in case it was set through an action
    contentType = null
    // Try to render a ScalateTemplate if no route matched
    findTemplate(requestPath) map { path =>
      contentType = "text/html"
      layoutTemplate(path)
    } orElse serveStaticResource() getOrElse resourceNotFound()
  }
}

case class Person(id: Long, name: String, age: Int)

scalatra-lift-jsonを利用した場合との違いは、以下の3点だけです。

json4sとscalatra-jsonのモジュールをインポート

scalatra-lift-jsonで使っていたlift-jsonモジュールのインポートの代わりに、scalatra-jsonのモジュールをインポートします。

scalatra-lift-json

import liftjson.LiftJsonSupport
import net.liftweb.json._

scalatra-json

import org.json4s._
import org.scalatra.json._

ミックスインするJSONヘルパートレイトを変更

JSONヘルパートレイトをscalatra-lift-jsonで提供されるlift-jsonのものから、scalatra-jsonで提供されるJacksonもしくはネイティブのものに変更します。

scalatra-lift-json

class JsonTestServlet extends ScalatraServlet with ScalateSupport with LiftJsonSupport

scalatra-json

// Jackson
class JsonTestServlet extends ScalatraServlet with ScalateSupport with JacksonJsonSupport
// or
// ネイティブ
class JsonTestServlet extends ScalatraServlet with ScalateSupport with NativeJsonSupport

Formatsの暗黙のパラメータを宣言

JacksonJsonSupport(NativeJsonSupport)がミックスインしているJValueResultトレイトの、implicitな抽象メソッドであるjsonFormatsが実装されていません。valで宣言して実装します。

protected implicit val jsonFormats: Formats = DefaultFormats

後はscalatra-lift-jsonと全く同じ構文で利用できます。ケースクラスへの抽出もそのまま使えるので、JSON-Objectマッピングのための面倒なコードを書く必要がないのが嬉しいです。

まとめ

最初にこの変更をリリースノートで見た際は、lift-jsonから変える必要があったのかなと思いました。しかし、json4sのドキュメントに書いてありましたが、lift-jsonはScalaフレームワークであるLiftのJSONユーティリティとして提供されていることもあり、新しいバージョンのScalaへの対応はLift本体のリリーススケジュールにどうしても引っ張られてしまいます。Liftは使ったことがないのであまりよく知りませんが、大きめのフレームワークということもあってか、新しいバージョンのScalaへの対応があまり早くないようです。

Scalatraでも、lift-jsonに依存して新しいバージョンのScalaにJSONライブラリが対応できないといったことを避けるため、今回の変更をしたものと思われます。json4sは既にScala 2.10に対応したモジュールがMavenリポジトリに公開されているようですので、JSONを扱うScalatraプロジェクトもScalatra 2.2系を使えばScala 2.10に乗せかえることができます。

余談ですが、Scalatra2.2系からサーブレットを登録するLifeCycle継承クラスの名前が、ScalatraからScalatraBootstrapに変更されたようです。名前がScalatraのままだとサーバー起動時に警告が出ますので、JSONライブラリの乗せかえのついでにクラス名を変更しておきましょう。

参考サイト

Scalatra

json4s

JSON4Sの日本語Readme 私訳

Scalatra+scalatra-lift-jsonでJSON