少年易酔學難成

IT/技術的な話題について書きます

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

Readsをどのように書くか

前回のエントリでplay-jsonのReads/Writesの意味とPlay Frameworkでの使い方を確認したので、実際にReads/Writesをどのように書くかを考えてみる。 今回はReads。

愚直にReadsを書く

JsValue => Aの処理を最も愚直に書けば以下のようになる。

case class SampleJson(
  f1: String,
  f2: Int,
  f3: NestedJson
)

case class NestedJson(
  f4: String
)

implicit val nestedJsonReads: Reads[NestedJson] = Reads[NestedJson] {
  case JsObject(fields) => fields.get("f4").map(StringReads.reads(_).fmap(NestedJson)).getOrElse(JsError(__ \ "f4", "error.required"))
  case _                => JsError(__, "error.expected.jsobject")
}

implicit val sampleJsonReads: Reads[SampleJson] = Reads[SampleJson] {
  case JsObject(fields) =>
    (
      fields.get("f1").map(StringReads.reads(_)).getOrElse(JsError(__ \ "f1", "error.required")),
      fields.get("f2").map(IntReads.reads(_)).getOrElse(JsError(__ \ "f2", "error.required")),
      fields.get("f3").map(nestedJsonReads.reads(_)).getOrElse(JsError(__ \ "f3", "error.required")).repath(__ \ "f3")
    ) match {
      case (JsSuccess(f1, _), JsSuccess(f2, _), JsSuccess(f3, _)) => JsSuccess(SampleJson(f1, f2, f3))
      // フィールドのどれか一つの読み取りがエラーのケース
      case (err@JsError(_),   JsSuccess(_, _),  JsSuccess(_, _))  => err
      case (JsSuccess(_, _),  err@JsError(_),   JsSuccess(_, _))  => err
      case (JsSuccess(_, _),  JsSuccess(_, _),  err@JsError(_))   => err
      // 二つのフィールドの読み取りがエラーのケース
      case (err1@JsError(_), err2@JsError(_), JsSuccess(_, _))    => JsError.merge(err1, err2)
      case (err1@JsError(_), JsSuccess(_, _), err2@JsError(_))    => JsError.merge(err1, err2)
      case (JsSuccess(_, _), err1@JsError(_), err2@JsError(_))    => JsError.merge(err1, err2)
      // 全てのフィールドの読み取りがエラーのケース
      case (err1@JsError(_), err2@JsError(_), err3@JsError(_))    => JsError.merge(JsError.merge(err1, err2), err3)
    }
  case _                => JsError(__, "error.expected.jsobject")
}

この書き方だと見通しが悪くなるし、フィールドの数が増えた場合に対応するのは大変だ。 二つのJsResultをマージする関数を作り、それを元に3つ、4つのJsResultをマージする方が簡単だろう。

def apply2[A, B, C](r1: JsResult[A], r2: JsResult[B])(f: (A, B) => C) = {
  case (e1@JsError(_), e2@JsError(_))       => JsError.merge(e1, e2)
  case (e@JsError(_),  _)                   => e
  case (_,             e@JsError(_))        => e
  case (JsSuccess(v1, _), JsSuccess(v2, _)) => JsSuccess(f(v1, v2))
}

def apply3[A, B, C, D](r1: JsResult[A], r2: JsResult[B], r3: JsResult[C])(f: (A, B, C) => D) = (apply2(r1, r2)((_, _)), r3) match {
  case (e1@JsError(_), e2@JsError(_))             => JsError.merge(e1, e2)
  case (e@JsError(_),  _)                         => e
  case (_,             e@JsError(_))              => e
  case (JsSuccess((v1, v2), _), JsSuccess(v3, _)) => JsSuccess(f(v1, v2, v3))
}

implicit val sampleJsonReads: Reads[SampleJson] = Reads[SampleJson] {
  case JsObject(fields) =>
    apply3(
      fields.get("f1").map(StringReads.reads(_)).getOrElse(JsError("error.required")).repath(__ \ "f1"),
      fields.get("f2").map(IntReads.reads(_)).getOrElse(JsError("error.required")).repath(__ \ "f2"),
      fields.get("f3").map(nestedJsonReads.reads(_)).getOrElse(JsError("error.required")).repath(__ \ "f3")
    )(SampleJson)
  case _                => JsError(__, "error.expected.jsobject")
}

もちろん、上記のように愚直にReadsを書くことはほとんどないだろう。 これは単にJsValueJsResultがどのようなものか理解するためだけのものだ。

FunctionalBuilderやJsPathを使って書く

もちろん、apply2, apply3を自前で用意する必要はない。 JsResultplay.api.libs.functional.FunctionalBuilderandを使えばいい。

implicit val sampleJsonReads: Reads[SampleJson] = Reads[SampleJson] {
  case JsObject(fields) =>
    (
      fields.get("f1").map(StringReads.reads(_)).getOrElse(JsError("error.required")).repath(__ \ "f1") and
      fields.get("f2").map(IntReads.reads(_)).getOrElse(JsError("error.required")).repath(__ \ "f2") and
      fields.get("f3").map(nestedJsonReads.reads(_)).getOrElse(JsError("error.required")).repath(__ \ "f3")
    )(SampleJson)
  case _                => JsError(__, "error.expected.jsobject")
}

まだコードにはボイラープレートが残っている。 一つはJsObjectとその他の場合のパターンマッチ。 二つめはfields.get(...).map(...).getOrElse(JsError("error.required")).repath(__ \ ...)の箇所だ。

この問題を解決する方法がJsPathにある。JsPathJsValueへのパスを表現するデータ型だ。 JsPathにあるread[T]を使えば以下のように書ける。

implicit val sampleJsonReads: Reads[SampleJson] = (
  (__ \ "f1").read[String] and
  (__ \ "f2").read[Int] and
  (__ \ "f3").read[NestedJson]
)(SampleJson)

read[T]は暗黙のパラメータとしてReads[T]を要求するので、String/Int/NestedJsonのReadsがimplicitのスコープ内でimplicit付きで定義されている必要がある。(明示的に渡す必要はない)

実はこの書き方だと渡されたJsValueがJsObjectではなかった時のエラーメッセージが"error.path.missing"になるという細かい違いがあるのだが、それはここでは無視する。 また、Option[T]マッピングするような任意項目の読み取りをしたい場合はreadNullable[T]を使えば"error.path.missing"で怒られることはない。

これでだいぶスッキリした書き方になった。

え?いちいちパスを書くのも面倒くさい?SampleJsonのフィールド名から組み立てられないのか? もちろん、その要求に応える方法も、ある。

macroを使った書き方

play-jsonJsonオブジェクトにはReads/Writesをマクロで構築する便利なメソッドがいくつか定義されている。 それを用いると以下のように書ける。

implicit val nestedJsonReads: Reads[NestedJson] = Json.reads[NestedJson]
implicit val nestedJsonReads: Reads[NestedJson] = Json.reads[SampleJson]

わずか二行でReads[SampleJson]の作成に必要な全てのコードが書けてしまった。

まとめ

Readsの書き方を、愚直な書き方から怠惰な書き方まで一通り追った。

もし、怠惰な書き方でうまく行かないような場面に遭遇しても、最悪愚直な書き方に戻って実現すればいいと思えば心健やかにコーディングできるはずだ。 多分、そういった心のゆとりがある方がスマートなやり方を思いつけるはずだ、、、。