Magnolia Tech

いつもコードのことばかり考えている人のために。

Scalatra-JSONの使い方

ScalatraのJSONサポート

ScalatraにはJSONのサポートが用意されていていて、JSONのリクエストやレスポンスにまつわる種々の機能を提供してくれます。

一応公式ドキュメントにはざっと使い方が説明されていますが、ちょっと端折り過ぎ感が有るのと、内部構造を理解した方がメリットをより理解できる部分も有るので、改めて使い方をまとめてみました。

長くなってきたので、今回は環境準備と、リクエストのJSONをパースするところまでです。JSONのレスポンスを生成する方は次回に。

2018/5/20 18:30追記

Json4sがパースエラーを起こした時の挙動が正確ではなかったので、修正しました。例外はWebアプリケーションまで伝搬せず、JNothingが返って来るだけです。

環境準備

プロジェクト作成

まずはsbt newコマンドでScalatraプロジェクトのひな形を用意します。

$ sbt new scalatra/scalatra.g8

色々と質問が出てきますが、今回は全てデフォルトのままで進めましょう(つまり、全てリターンキーを押下して進める)。

ちなみにsbt newコマンドはgitリポジトリの作成までは面倒を見てくれないので、プロジェクトが作成されたら忘れずにgit initをしておきましょう。

build.sbtの追記

build.sbtに必要なアーティファクトを追加します。

ScalatraのJSONモジュールはscalatra-jsonという別モジュールで提供されているので、まずはそれを追加します。バージョンはScalatra本体に必ず合わせて下さい。

またscalatra-jsonjson4sをベースで作られていて、JSONパーサとしてJacksonか、Lift-Json由来のNativeの2種類のうち、どちらかを選択して使用するようになっています。

どちらを使用するかに合わせてbuild.sbtに下記のアーティファクトを追加します。

  • Jacksonをパーサに使う場合は、json4s-jacksonを指定
  • Nativeパーサを使う場合は、json4s-nativeを指定

なお、json4sのバージョンはScalatraが内部で指定しているバージョンに合わせる必要が有ります。例えばScalatra 2.6.3はjson4s 3.5.2を採用しているので、同じようにbuild.sbtには3.5.2を指定する必要が有ります。バージョンが不一致の場合コンパイルエラーになります。この辺りはドキュメントには書かれていないので、必ずScalatra本体のDependencies.scalaを参照して下さい。

Scalatra 2.6.3で、Jacksonパーサーを使う場合のbuild.sbtは以下のようになります。

val ScalatraVersion = "2.6.3"

libraryDependencies ++= Seq(
  "org.scalatra" %% "scalatra" % ScalatraVersion,
  "org.scalatra" %% "scalatra-json" % ScalatraVersion,
  "org.json4s" %% "json4s-jackson" % "3.5.2",
...
)

これで準備ができました。

JSONを受け取る

HTTP RequestにJSONが含まれている場合、parsedBodyというメソッドを使うとrequestにJSONが含まれているか否かの判定と、JSONのパースまでを一気にやってくれるので便利です。

parsedBodyの使い方

先ほどの環境設定でパーサにJacksonとNativeのうち、どちらを利用するか選択しましたが、選択したパーサに合わせて、JacksonJsonSupport trait又はNativeJsonSupport traitをmix-inします。

例えばJacksonJsonSupportを使う場合は、以下のようなコードになります。

jsonFormatsは、json4sのパーサが必要とする変換方法の指定です。

package com.example.app

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

import org.json4s._
import org.json4s.{DefaultFormats, Formats}

class MyScalatraServlet extends ScalatraServlet with JacksonJsonSupport {
  protected implicit lazy val jsonFormats: Formats = DefaultFormats

  post("/") {
    val ast = parsedBody

    ...
  }

parsedBodyContent-TypeJSONの場合にのみパースを行うので、もしJSON以外が指定されている場合はJNothing(Json4sでのNone型に相当)が返ります。また、正常にパースできない、つまりinvalidなJSONデータの場合もJNothingが返ります。

そのため、JNothingが返却される可能性について対応するコードを用意しておく必要が有ります(この肝心なことが公式ドキュメントに書かれていない!!)

ちなみに、Json4sはパースエラー時には例外を送出しますが、Scalatra内でその例外をキャッチしエラーログへ出力し、JNothingを返しているので、Webアプリケーション側には例外は返ってきません。パースエラーになった原因はログから追跡しましょう。

case classにマッピングする

parsedBodyはJson4sのASTを返すメソッドなので、ASTが得られれば後は通常のJson4sの使い方と同じようにJSONデータをcase class等にマッピングして使用します。

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

class MyScalatraServlet extends ScalatraServlet with JacksonJsonSupport {
  protected implicit lazy val jsonFormats: Formats = DefaultFormats

  post("/create") {
    val ast = parsedBody

    val person = ast.extract[Person]

    person.name
  }

case classにマッピングできない場合は例外が送出されますので、やはり例外に対応したコードを書いておく必要が有ります。ここは素のJson4sなので、Json4sのドキュメントを参照し、色々と試してみて下さい。

またJson4sのcase classへのマッピングはリフレクションを使用していますが、リフレクションの制約によりクラス内で定義されたcase classでは正しく動作しません。必ずトップレベルでcase classを定義するようにして下さい。小さなアプリケーションではうっかりServlet Classの中でcase classを定義してしまいそうになりますが、それは誤りです(初めて使ったとき、これが分からず数時間悩みました)。

下記のissueが参考になるでしょう。

https://github.com/json4s/json4s/issues/125

JsonValueReader

パースしたASTを簡易にサーチするためのヘルパークラスとしてJsonValueReaderというクラスが用意されています。パスをドットで連結したものをreadメソッドに渡すと、ASTを辿って行って、オブジェクトを取得してくれます。

case class Person(id: Int, name: String)
case class Group(name: String, person: Person)

class MyScalatraServlet extends ScalatraServlet with JacksonJsonSupport {
  protected implicit lazy val jsonFormats: Formats = DefaultFormats

  post("/create") {
    val ast = parsedBody
    val reader = new JsonValueReader(ast)
    val name = reader.read("person.name")

    name
  }
}

ドキュメントもテストもないので、これを使うくらいだったらJson4sが提供するクエリ構文を使った方がお勧めです。

XML

Json4sはJSONXMLの相互変換をサポートしているため、scalatra-jsonでもXMLをサポートしています。XMLをリクエストで受け取った場合は、前述の流れと同じようにJson4sのJSON ASTを得ることができます(Content-Typeがxml、つまり'application/xml'でもparsedBodyは有効です)。

ですが、Json4sを使わなくても…という感じなので、そこは普通にXMLとしてパースした方が良いでしょう。

Content-Typeの指定

parsedBodyはContent-Typeヘッダを元にJSONか否かを判定しています(具体的にはapplication/json)。そのため、例えばContent-Typeにapplication/x-www-form-urlencodedが指定されていると、body部をJSONとしてパースはしません(判別できないので当たり前ですが…)。更にServletの仕様により、bodyの内容がパラメータに格納されているパターンも有るので要注意です。

blog.magnolia.tech

クライアントからのContent-Typeに注意しましょう(特にcurlはPOST時はapplication/x-www-form-urlencodedがデフォルトです)。

おわりに

長くなってきたので、ここまで。次回はJSONのレスポンスを生成する箇所をやります。

ScalatraのJSONサポートはJson4sに強く依存し過ぎていて(XMLサポートとか必要?)、昨今の「リフレクションを使っているJson4sを避けた方が良い」という流れも有って、別のJSONモジュールをベースに作り直した方が良い、という時期に来ています(Scalatra-Json2?)。

JSONレスポンスの解説が終わったら、ScalaJsonモジュール一覧の紹介をやろうと思います。

Scalatra in Action

Scalatra in Action

  • 作者: Ivan Porto Carrero,Ross A. Baker,Dave Hrycyszyn,Stefan Ollinger,Jared Armstrong
  • 出版社/メーカー: Manning Pubns Co
  • 発売日: 2014/01/28
  • メディア: ペーパーバック
  • この商品を含むブログを見る

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)