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-json
はjson4sをベースで作られていて、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 ... }
parsedBody
はContent-Type
がJSONの場合にのみパースを行うので、もし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はJSONとXMLの相互変換をサポートしているため、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の内容がパラメータに格納されているパターンも有るので要注意です。
クライアントからのContent-Typeに注意しましょう(特にcurl
はPOST時はapplication/x-www-form-urlencoded
がデフォルトです)。
おわりに
長くなってきたので、ここまで。次回はJSONのレスポンスを生成する箇所をやります。
ScalatraのJSONサポートはJson4sに強く依存し過ぎていて(XMLサポートとか必要?)、昨今の「リフレクションを使っているJson4sを避けた方が良い」という流れも有って、別のJSONモジュールをベースに作り直した方が良い、という時期に来ています(Scalatra-Json2?)。
JSONレスポンスの解説が終わったら、ScalaのJsonモジュール一覧の紹介をやろうと思います。
- 作者: 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)
- 作者: 山本陽平
- 出版社/メーカー: 技術評論社
- 発売日: 2010/04/08
- メディア: 単行本(ソフトカバー)
- 購入: 143人 クリック: 4,320回
- この商品を含むブログ (183件) を見る