playframeworkのJsonのEitherのReadsやWritesやFormat

これはPlay framework Advent Calendar 2014の24日目です

アドベントカレンダー空いてたのと、たまたま思い出したので、また 前回書いたものと関係あるような play-jsonのReadsやWritesの話をします。


といっても、前回にくらべれば短いです。*1だいたい以下のコードで言いつくされている。

そういえばEitherのReadsやWritesって、現状play内部には存在してないし、たまに作って使ったことあるけど、blogとかに貼ったことなかった気がするので。この場合のEitherは、成功か失敗かという使い方ではなく、単に2つのうちのどちらかを表現してるという使い方です。
ググっても、一発でわかりやすいのが出ないようですね?


playのMLに質問きてたので、gistのURLをはりつけるだけの回答をしておきました

https://gist.github.com/xuwei-k/e439479b3b4a6ac04322
https://groups.google.com/d/topic/play-framework/anYvYB-8GYs/discussion

上記では、Readsが失敗した場合2つのエラーを単にmergeしてますが、そうではなく「2つそれぞれを試したけど、両方失敗しました」という感じのメッセージを自作して埋め込んだほうが親切かもしれません?そのあたりは各自で工夫してください。

あと、上記のReadsの実装は「片方を試して成功したら、もう片方は試さないで最初の成功を返す」ですが「Readsが2つとも成功してしまったら、逆にエラーにする」という挙動が欲しい場合もあり得るかもしれません。*2その場合は以下

def eitherReads[A, B](implicit A: Reads[A], B: Reads[B]): Reads[Either[A, B]] =
  Reads{ json =>
    (A.reads(json), B.reads(json)) match {
      case (JsSuccess(l, _), _: JsError) =>
        JsSuccess(Left(l))
      case (_: JsError, JsSuccess(r, _)) =>
        JsSuccess(Right(r))
      case (JsSuccess(_, _), JsSuccess(_, _)) =>
        JsError("両方成功しちゃったのでエラーにしたよ(・ω<)") // ここのメッセージはご自由に。引数でとるようにしてもいいかも
      case (e1: JsError, e2: JsError) =>
        e1 ++ e2
    }
  }

FormatやOFormatは作ってないけれど、play側にimplicitがあるので、直接定義しなくてもいけるはずです。

また、「Scala標準のEitherは使いづらいので使いたくない」とか「コンパニオンオブジェクトにimplicitで定義したい(そのほうがデフォルトでスコープに入って便利)」という需要があれば、いっそのことEitherと同型のclassを自作してもいいかもしれません*3


「毎回ReadsとWritesからFormatのインスタンスが生成されるのが微妙に無駄でいやだ!」
とか
「(整合性保ちやすいように)そもそもReadsやWrites片方だけでなく、必ず両方を定義する決まりにしてるんだ!」
などのこだわりがあれば、逆にReadsとWritesをimplicitで定義するのではなく、FormatやOFormatだけを定義したほうがいいかもしれません。
もし「インスタンスの生成コスト」まで気になるなら、以下のように「implicitを使わずにFormatを定義」しておいて、明示的に呼び出して結果をvalで束縛とかしましょう。

  // implicit つけずに定義
  def eitherFormat[A, B](implicit A: Format[A], B: Format[B]): Format[Either[A, B]] =

  case class Foo( /* 定義は略 */ )
  object Foo {
     implicit val format: Format[Foo] = // 略
  }
  case class Bar( /* 定義は略 */ )
  object Bar {
     implicit val format: Format[Bar] = // 略
  }
  
  // 明示的に呼び出してvalで束縛すれば、毎回生成されずに済む
  implicit val fooOrBar: Format[Either[Foo, Bar]] = eitherFormat[Foo, Bar]

*1:短いつもりだったが、後半色々説明書いたらそれなりに長くなった感もある・・・

*2:ただし、両方が成功するのがありえない、と最初からわかっている場合は、必要以上に2回無駄にparseするだけなので使わないほうがよい

*3:というか、気づいたら仕事のコードがそうなってしまった・・・