少年易酔學難成

酒に交われば朱くなる

play-jsonについて学ぶ(1)

目的

現在のプロジェクトではJSONライブラリとしてplay-jsonを(play scalaと共に)使用している。 play-jsonはドキュメントは豊富なのでググれば大抵の問題は解決できて便利なのだけれども、ググってもやり方が見つからない場合は解決に時間がかかってしまっていた。 これはplay-jsonの基本を理解していなかったからだと思っていて、そこでplay-jsonソースコードを読み込んでみた。

この記事の目的はそのとき理解したことの整理だ。

play-jsonの基本

JSONの読み込み

JSONを読み込み、任意のクラスAにマッピングする処理は次の二段階で行われる。

  1. JSONJsValueへ変換する
  2. JsValueJsResult[A]に変換する(ここでAは任意のクラス)

JsValueJSONの抽象構文木で、JSONの値型に対応するサブクラス(JsNull, JsObject, JsArray, JsBoolean, JsString, JsNumber)が存在する。 JsResult[A]Aへの変換結果を表すクラスで、成功すればJsSuccess[A]、失敗すればJsErrorとなる、いわばEither[L, R]に似たクラスだ。

1の変換はJson.parseによって行われ、2の変換はReads[A]によって行われる。 ここでJSONの文法に合致しない不正なJSONが与えられた場合、Json.parseで使われているJacksonの例外(JsonParseExceptionなど)がスローされる。 2でJsValueからAへの変換に失敗した場合は例外ではなくJsErrorで通知される。JsErrorはその失敗原因をerrors: Seq[JsonValidationError]によって保持している。

Json.parseドメインに依存しない処理なので、ユーザはReads[A]を定義することによって目的の処理を得る。

JSONの書き出し

JSONの書き出しも読み込みと同じくJsValueを介した二段階で行われる。

  1. 任意のクラスAJsValueに変換する
  2. JsValueJSONに変換する

1の変換はWrites[A]もしくはOWrites[A]によって行われ、2の変換はJson.stringifyなどで行われる(整形の有無などいくつかのメソッドがある)。 Writes[A]OWrites[A]の違いは、Writes[A]AJsValueに変換するがOWrites[A]AJsObjectに変換するというところだ。 OWrites[A]Writes[A]を継承(trait OWrites[-A] extends Writes[A])している。

ほとんどの場合JSONはトップレベルはObjectとなるので、OWrites[A]の使用が妥当であるように思えるが、後述するような小さなWrites[A]を組み合わせて大きなWrites[A]を作っていくスタイルであればWrites[A]を使用した方が良いだろう。

読み込みの場合と同じく、Json.stringifyドメインに依存しない処理となるので、ユーザはWrites[A]を定義することによって目的の処理を得る。

Play FrameworkのActionにおけるplay-json

Reads/Writesのイメージを固めるために、Play Frameworkでのplay-jsonの典型的な使用例を挙げる。

implicit val sampleJsonReads: Reads[SampleJson] = Json.reads[SampleJson] // Reads[SampleJson]の定義。定義の意味については後述する。

def sample: Action[JsValue] = Action.async(parse.json) { implicit request =>
  request.body.validate[SampleJson].fold(
    errs => Future.successful(BadRequest), // JsErrorの場合の処理(errsはSeq[JsonValidationError]型)
    json => { // JsSuccessの場合の処理(jsonはSampleJson型)
      // 何か処理
    }
  )
}

Action[A]Request[A]を受け取ってResultを返すRequest[A] => Resultの関数だ。 Request[A]A型に変換されたbodyを持つリクエストという意味なので、このサンプルでのrequest.bodyJsValueとなる。 JsValue#validate[A]は暗黙的(implicit)にReads[A]を引数にとりJsResult[A]を返す。 大抵の場合はJsResult[A]のfoldを使って異常系と正常系の処理を記述する。

まとめ

ここまでが、play-jsonのReads/Writesを理解する上での基本だと考えている。 自分はこれを理解するまで、Readsを書くのにとても苦労していた。

ということで、次回はReadsの書き方についてまとめてみようと思う。