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
を書くことはほとんどないだろう。
これは単にJsValue
やJsResult
がどのようなものか理解するためだけのものだ。
FunctionalBuilderやJsPathを使って書く
もちろん、apply2
, apply3
を自前で用意する必要はない。
JsResult
はplay.api.libs.functional.FunctionalBuilder
のand
を使えばいい。
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
にある。JsPath
はJsValue
へのパスを表現するデータ型だ。
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-jsonのJson
オブジェクトにはReads/Writesをマクロで構築する便利なメソッドがいくつか定義されている。
それを用いると以下のように書ける。
implicit val nestedJsonReads: Reads[NestedJson] = Json.reads[NestedJson] implicit val nestedJsonReads: Reads[NestedJson] = Json.reads[SampleJson]
わずか二行でReads[SampleJson]
の作成に必要な全てのコードが書けてしまった。
まとめ
Readsの書き方を、愚直な書き方から怠惰な書き方まで一通り追った。
もし、怠惰な書き方でうまく行かないような場面に遭遇しても、最悪愚直な書き方に戻って実現すればいいと思えば心健やかにコーディングできるはずだ。 多分、そういった心のゆとりがある方がスマートなやり方を思いつけるはずだ、、、。