§JSON Reads[T]/Writes[T]/Format[T] コンビネータ
このドキュメントは、当初 Pascal Voitot (@mandubian) の記事 mandubian.com として公開されたものです
§Play2.1
における新機能の概要
Reads[T]
/Writes[T]
/Format[T]
コンビネータは、JsPath とシンプルな論理演算子に基づいています
import play.api.libs.json._
import play.api.libs.functional.syntax._
val customReads: Reads[(String, Float, List[String])] =
(JsPath \ "key1").read[String](email keepAnd minLength(5)) and
(JsPath \ "key2").read[Float](min(45)) and
(JsPath \ "key3").read[List[String]]
tupled
Reads[T]
API は、JsSuccess[T]
か、あるいはすべてのバリデーションエラーを集約したJsError
として、モナディックなJsResult[T]
を返すことにより JSON のバリデーションを行います
import play.api.libs.json.Json
val js = Json.obj(
"key1" -> "alpha",
"key2" -> 123.345F,
"key3" -> Json.arr("alpha", "beta")
)
scala> customReads.reads(js)
res5: JsSuccess(("alpha", 123.345F, List("alpha", "beta")))
customReads.reads(js).fold(
invalid = { errors => ... },
valid = { res =>
val (s, f, l): (String, Float, List[String]) = res
...
}
)
それでは詳細に踏み込んでみましょう ;)
§JsPath とは
パスに基づいたシンプルな構文を使って XML 抽象構文木のノードにアクセスすることのできる XMLPath
についてはご存知でしょう。
JSON は抽象構文木なので、同じような構文を適用することができます。そして、これを論理的に JsPath
と呼んでいます。
以下のすべての例では、ひとつ前の段落で定義した JSON を使います。
§JsPath を構築する
import play.api.libs.json._
// シンプルなパス
JsPath \ "key1"
// 二階層のパス
JsPath \ "key3" \ "key33"
// インデックス付けされたパス
(JsPath \ "key3" \ "key32")(2) // JsArray の二番目の要素
// 複数の/再帰的なパス
JsPath \\ "key1"
§代替構文
JsPath
という構文もかなりかっこいいのですが、コード中において Reads[T]
をよりクリアに目立たせることのできる構文を見つけました。
このような理由から、JsPath
のエイリアスを提供しています: __
(二つのアンダースコア) です。
この構文を使っても使わなくても構いません。この構文は、コードの中から JsPath をすぐに見つけられるようにする、視覚的な利便性に過ぎません
次のように書くことができます:
import play.api.libs.json._
import play.api.libs.functional.syntax._
// シンプルなパス
__ \ "key1"
// 二階層のパス
__ \ "key3" \ "key33"
// インデックス付けされたパス
(__ \ "key3" \ "key32")(2) // JsArray の二番目の要素
// 複数のパス
__ \\ "key1"
// 双方の構文の違いを紹介する Reads[T] コンビネータのサンプル
// 今すぐこのコードを理解しようとしないでください… これは次の段落で説明します
val customReads =
(JsPath \ "key1").read(
(JsPath \ "key11").read[String] and
(JsPath \ "key11").read[String] and
(JsPath \ "key11").read[String]
tupled
) and
(JsPath \ "key2").read[Float](min(45)) and
(JsPath \ "key3").read(
(JsPath \ "key31").read[String] and
(JsPath \ "key32").read[String] and
(JsPath \ "key33").read[String]
tupled
)
tupled
// __ を使った場合
val customReads =
(__ \ "key1").read(
(__ \ "key11").read[String] and
(__ \ "key11").read[String] and
(__ \ "key11").read[String]
tupled
) and
(__ \ "key2").read[Float](min(45)) and
(__ \ "key3").read[List[String]] (
(__ \ "key31").read[String] and
(__ \ "key32").read[String] and
(__ \ "key33").read[String]
tupled
)
tupled
// JSON ツリーの構造がひと目で分かりますね
§JsPath の値へのアクセス
与えられた JsPath にある JsValue から値を取り出す重要な関数は以下のとおりです:
sealed trait PathNode {
def apply(json: JsValue): List[JsValue]
…
}
ご覧のとおり、この関数は List[JsValue]
を取り出します
これは、以下のようにシンプルに使うことができます:
import play.api.libs.json._
// JsPath を構築する
scala> (__ \ "key1")(js)
res12: List[play.api.libs.json.JsValue] = List("value1") // これは実際には JsString("value1") です
// 二階層のパス
scala> (__ \ "key3" \ "key33")(js)
res13: List[play.api.libs.json.JsValue] = List({"key":"value2","key34":"value34"})
// インデックス付けされたパス
scala> (__ \ "key3" \ "key32")(2)(js)
res14: List[play.api.libs.json.JsValue] = List(234.13)
// 複数のパス
scala> (__ \\ "key1")(js)
res17: List[play.api.libs.json.JsValue] = List("value1", "value2")
§Reads[T] はバリデータに
§Play2.0.x における Reads
Play2.0.x
では、どのように Json Reads[T]
を書いたか覚えていますか?reads
関数をオーバーライドする必要がありました。
trait Reads[A] {
self =>
/**
* JsValue を A に変換する
*/
def reads(json: JsValue): A
}
以下のような、JSON 構造をマッピングするシンプルなケースクラスを取り上げます:
case class Creature(
name: String,
isDead: Boolean,
weight: Float
)
Play2.0.x
では、以下のような reader を書いていたことでしょう:
import play.api.libs.json._
implicit val creatureReads = new Reads[Creature] {
def reads(js: JsValue): Creature = {
Creature(
(js \ "name").as[String],
(js \ "isDead").as[Boolean],
(js \ "weight").as[Float]
)
}
}
scala> val js = Json.obj( "name" -> "gremlins", "isDead" -> false, "weight" -> 1.0F)
scala> val c = js.as[Creature]
c: Creature("gremlins", false, 1.0F)
簡単ですよね?
こんなに簡単なら、何が問題なのでしょう?
以下のような、フィールドが不足した JSON を渡すことを想像してください:
val js = Json.obj( "name" -> "gremlins", "weight" -> 1.0F)
なにが起こるでしょう?
java.lang.RuntimeException: Boolean expected
at play.api.libs.json.DefaultReads$BooleanReads$.reads(Reads.scala:98)
at play.api.libs.json.DefaultReads$BooleanReads$.reads(Reads.scala:95)
at play.api.libs.json.JsValue$class.as(JsValue.scala:56)
at play.api.libs.json.JsUndefined.as(JsValue.scala:70)
これは何でしょう?
そうです、みっともない (サブクラスですらない) RuntimeException です。しかし、JsValue.asOpt[T]
を使ってこれに対処することができます :)
scala> val c: Option[Creature] = js.asOpt[Creature]
c: None
クールですが、分かるのは Json => Creature
のデシリアライズに失敗したことだけで、どこ、あるいはどのフィールドで失敗したのかは分かりません
§Play2.1 における Reads
この不完全な API を Play2.1
でそのままにしておけず、 Reads[T]
API を以下のように変更しました :
trait Reads[A] {
self =>
/**
* JsValue を A に変換する
*/
def reads(json: JsValue): JsResult[A]
}
そうです、すでに存在する自作の Reads は、すべてリファクタリングしなければなりません。しかし、たくさんの興味深い新機能が手に入ることがすぐに分かると思います…
すぐに、今回の問題に適用した Either にちょっと似ているように見える、とてもシンプルな構造の JsResult[A]
に目が行ったことと思います。
JsResult[A]
は二つのタイプになり得ます:
reads
が成功した場合はJsSuccess[A]
case class JsSuccess[A](
value: T, // JsValue => A のデシリアライズが動作したときに取り出される値
path: JsPath = JsPath() // この A が JsValue に読み込まれたときのルートの JsPath (デフォルトでは JsValue のルート)
) extends JsResult[T]
// 値から JsSuccess を作るには、次のようにシンプルに行います:
val success = JsSuccess(Creature("gremlins", false, 1.0))
reads
が失敗した場合はJsError[A]
JsError は累積エラーであり、異なる JsPath にある Json で検出された複数のエラーを保存することができます。この素晴らしい利点に注目してください
case class JsError(
errors: Seq[(JsPath, Seq[ValidationError])]
// errors は、この JsValue のどこにエラーがあったのか、
// このパスのどこにバリデーションエラーがあったのかを
// 指し示す JsPath のシーケンスです
) extends JsResult[Nothing]
// ValidationError は、引数を伴った (ローカライズされたメッセージにマッピングできる) メッセージです
case class ValidationError(message: String, args: Any*)
// JsError を作るために、例えば次のようないくつかのヘルパーがあります
val errors1 = JsError( __ \ 'isDead, ValidationError("validate.error.missing", "isDead") )
val errors2 = JsError( __ \ 'name, ValidationError("validate.error.missing", "name") )
// とても興味深いことに、Errors は累積することができます
scala> val errors = errors1 ++ errors2
errors: JsError(List((/isDead,List(ValidationError(validate.error.missing,WrappedArray(isDead)))), (/name,List(ValidationError(validate.error.missing,WrappedArray(name))))))
さて、ここで興味深いのは、JsResult[A]
はモナディックな構造であり、この構造の馴染み深い関数と共に使うことができるということです:
flatMap[X](f: A => JsResult[X]): JsResult[X]
fold[X](invalid: Seq[(JsPath, Seq[ValidationError])] => X, valid: A => X)
map[X](f: A => X): JsResult[X]
filter(p: A => Boolean)
collect[B](otherwise:ValidationError)(p:PartialFunction[A,B]): JsResult[B]
get: A
次のような糖衣構文と使うこともできます :
asOpt
asEither
- …
エラーを累積するので、
JsResult[A]
はモナディックなだけでなくアプリカティブである ことにも注目してください。
この累積する機能のため、JsResult[T]
を for 内包表記 で使うことはあまり良いことではありません。すべてのエラーではなく、最初のひとつだけを取り出すことになるからです。
§Reads[A] はバリデータに
ご理解頂いたとおり、新しい Reads[A]
を使うと、ただ JsValue を別の構造にデシリアライズするだけではなく、実際にその JsValue の バリデーションを行い 、そしてすべてのバリデーションエラーを探し出します。
ところで、JsValue
に validate
と呼ばれる新しい関数が登場しました:
trait JsValue {
…
def validate[T](implicit _reads: Reads[T]): JsResult[T] = _reads.reads(this)
// 以前と同じように振る舞いますが、具体的な実行時例外 JsResultException を投げるようになりました
def as[T](implicit fjs: Reads[T]): T
// 以前とまったく同じように振る舞います
def asOpt[T](implicit fjs: Reads[T]): Option[T]
…
}
// スコープから正しい implicit を取得できる場合に、このように書けるようになります
val res: JsResult[Creature] = js.validate[Creature])
§JsResult[A] の操作
JsResult
を操作する際は、値に直接アクセスするのではなく、map/flatmap/fold
を使って値を変更することが推奨されています。
import play.api.libs.json._
val res: JsResult[Creature] = js.validate[Creature]
// 成功/失敗を管理し、何かしらの結果を返します
res.fold(
valid = { c => println( c ); c.name },
invalid = { e => println( e ); e }
)
// name を直接取得します (JsResult が JsError の場合、get は NoSuchElementException を投げることができます)
val name: JsSuccess[String] = res.map( creature => creature.name ).get
// 結果をフィルタリングします
val name: JsSuccess[String] = res.filter( creature => creature.name == "gremlins" ).get
// お馴染みの Play アクション
def getNameOnly = Action(parse.json) { request =>
val json = request.body
json.validate[Creature].fold(
valid = ( res => Ok(res.name) ),
invalid = ( e => BadRequest(e.toString) )
)
}
§Reads の興味深い新機能
ご存知のとおり、Play2.1 の Json API はまだ草案であり、この記事を書き始めたときからも進化しています。
その後、いくつかの (概念的ではなく、表面的な) 変更が加えられました。
§Reads[A <: JsValue] andThen Reads[B]
andThen
には、関数合成に関する馴染み深い Scala のセマンティックが含まれています : JSON に Reads[A <: JsValue]
を適用して JsValue を取り出し、それからこの JsValue に Reads[B]
を適用します。
§Reads[A <: JsValue].map(f: A => B): Reads[B]
map
は馴染み深く、そしていつでもとても便利な Scala の map 関数です。
§Reads[A <: JsValue].flatMap(f: A => Reads[B]): Reads[B]
flatMap
は、馴染み深い Scala の flatMap 関数です。
§JsResult[A] で Reads[T] を書き直す
検出されたすべてのエラーをかき集めた JsResult を返さなければならないので、JsResult を返す Reads[A]
API を以前の Reads[A]
のように書くことはできません。
シンプルに flatMap で Reads[T] を組み立てるところを想像するかもしれません :
以下のコードは誤りです
import play.api.libs.json._
// 誤ったコードです。使わないでください
implicit val creatureReads = new Reads[Creature] {
def reads(js: JsValue): JsResult[Creature] = {
(js \ "name").validate[String].flatMap{ name =>
(js \ "isDead").validate[Boolean].flatMap { isDead =>
(js \ "weight").validate[Float].map { weight =>
Creature(name, isDead, weight)
}
}
}
}
}
JsResult
の主な目的は、JsValue のバリデーションを行っている間に見つかったすべてのエラーをかき集めることであることを思い出してください。
JsResult.flatMap
は純粋にモナディックな関数 (何のことか分からなくても、理解できるので気にしないでください) なので、flatMap()
に渡した関数は、その結果が JsSuccess
であるときのみ呼び出され、そうでない場合はただ JsError
を返します。
これは、上記のコードはバリデーションを行っている間に見つかったすべてのエラーをかき集めるわけではなく、最初のエラーで止まってしまうことを意味しており、これはまったく望ましくありません。
実際のところ、ただ Reads を組み立てるだけではなく、以下のようなスキーマに従って結合することを期待しているので、このような場合においてモナドパターンは好ましくありません:
Reads[String] AND Reads[Boolean] AND Reads[Float]
=> Reads[(String, Boolean, Float)]
=> Reads[Creature]
このため、Reads を結合できるようにする何かが必要であり、これこそが
Play2.1
が JSON のために用意した素晴らしい新機能 : JsPath を伴う READS コンビネータ です。
この要件に適応するように総称的な関数型の構造に基づいて実装する方法の、より論理的な側面を知りたい場合は、@sadache の書いたこのポスト “アプリカティブは制約し過ぎ。アプリカティブの打破と関数型ビルダの紹介” で読むことができます。
§Reads[T] コンビネータを書く
§コンビネータを使うための最小インポート
// スコープ内に Json 構造が必要な場合
import play.api.libs.json._
// 重要。必要なツールをスコープにインポートします
import play.api.libs.functional.syntax._
§コンビネータで Reads[T] を書き直す
いきなりサンプルに飛び込んで実践するのが往々にして最良です :
// 重要。必要なツールをスコープにインポートします
import play.api.libs.json._
import play.api.libs.functional.syntax._
implicit val creatureReads = (
(__ \ "name").read[String] and
(__ \ "isDead").read[Boolean] and
(__ \ "weight").read[Float]
)(Creature.apply _)
// または、apply 関数を持つコンパニオンオブジェクトと共にケースクラスとするのもシンプルな方法です
implicit val creatureReads = (
(__ \ "name").read[String] and
(__ \ "isDead").read[Boolean] and
(__ \ "weight").read[Float]
)(Creature)
// あるいは、Scala のパーサコンビネータを知っている方々は、これに着想を得た演算子を使います
implicit val creatureReads = (
(__ \ "name").read[String] ~
(__ \ "isDead").read[Boolean] ~
(__ \ "weight").read[Float]
)(Creature)
どうでしょう、難しいことは何もありませんよね?
§(__ \ "name")
は read[String]
を適用しようとしている JsPath
です
§and
は Reads[A] and Reads[B] => Builder[Reads[A ~ B]]
を意味する演算子に過ぎません
A ~ B
はCombine A and B
を意味しますが、どのように結合するかは前提としません (タプル、オブジェクト、その他どんなものにでもなることができます)Builder
は実際の型ではありませんが、and
演算子は直接Reads[A ~ B]
を作るのではなく、Reads[A ~ B]
を作れたり、または別のReads[C]
と結合できたりする中間成果物を作ることを述べるために紹介しています
§(…)(Creature)
は Reads[Creature]
を作ります
- 以下のことに気を付けてください:
(__ \ "name").read[String] and (__ \ "isDead").read[Boolean] and (__ \ "weight").read[Float]
これは、以下のものを作ります
Builder[Reads[String ~ Boolean ~ Float])]
しかし、期待しているのは Reads[Creature]
です。
- そのため、最終的に
Reads[Creature]
を取得するために、関数Creature.apply = (String, Boolean, Float) => Creature
コンストラクタにBuilder[Reads[String ~ Boolean ~ String])]
を適用します
以下を試してみてください:
scala> val js = Json.obj( "name" -> "gremlins", "isDead" -> false, "weight" -> 1.0F)
scala> js.validate[Creature]
res1: play.api.libs.json.JsResult[Creature] = JsSuccess(Creature(gremlins,false,1.0),)
// この JsPath はデフォルトでルートになるので、最後のカンマの後には何もありません
さて、ここでエラーがあった場合は何が起こるのでしょう?
scala> val js = Json.obj( "name" -> "gremlins", "weight" -> 1.0F)
scala> js.validate[Creature]
res2: play.api.libs.json.JsResult[Creature] = JsError(List((/isDead,List(ValidationError(validate.error.missing-path,WrappedArray())))))
分かり易いでしょう?
§複雑化された場合
ええ、あなたの考えていることは分かっています : もっと複雑な場合はどうでしょう。フィールドに複数の制約があって、Json の中に Json が組み込まれていて、再帰的なクラスで…
例に取り上げている生き物について想像してみましょう:
- それは email アカウントを持つ比較的モダンな生き物であり、その生き物自身だけが知っている理由によって、五文字より少ない email アドレスを嫌います
- お気に入りデータを二つ持つ場合があります:
- (それを受け入れるにはモンティ・パイソンが好き過ぎるために) “ni” であってはならず、そして最初の二文字がスキップされる、ひとつの (JSON では “string” と呼ばれる) String
- 86 より小さいか、または 875 より大きな (なぜかは聞かないでください。我々とは違う論理を持つ生き物なのです) ひとつの (JSON では “number” と呼ばれる) Int
- それには友達がいるかもしれません
- 多くの生き物はあまり社交的ではないので、とても必要なソーシャルアカウントをオプションで持っているかもしれません
クラスは以下のようになります:
case class Creature(
name: String,
isDead: Boolean,
weight: Float,
email: String, // email フォーマットかつ minLength(5)
favorites: (String, Int), // 間抜けなお気に入りデータ
friends: List[Creature] = Nil, // ええ、デフォルトでは友達がいません
social: Option[String] = None // デフォルトでは社交的ではありません
)
Play2.1
は多くの総称的な Reads ヘルパーを提供しています:
JsPath.read[A](implicit reads:Reads[A])
には、この JsPath にある JSON の内容に適用される独自のReads[A]
を渡すことができます。この性質により、JSON ツリー構造に対応する階層的なReads[T]
を組み立てることができます。JsPath.readNullable
は、見つからないか、空のフィールドを持つReads[Option[T]]
を許容しますReads.email
は、文字列が email フォーマットであることを検証しますReads.minLength(nb)
は、文字列長の最小値を検証しますReads[A] or Reads[A] => Reads[A]
演算子は、馴染み深いOR
論理演算子ですReads[A] keepAnd Reads[B] => Reads[A]
は、Reads[A]
とReads[B]
の実行を試みますが、Reads[A]
の結果のみを保持します (Scala のパーサコンビネータkeepAnd == <~
を知っている人のためのものです)Reads[A] andKeep Reads[B] => Reads[B]
は、Reads[A]
とReads[B]
の実行を試みますが、Reads[B]
の結果のみを保持します (Scala のパーサコンビネータandKeep == ~>
を知っている人のためのものです)
// Reads ヘルパーをスコープにインポートします
import play.api.libs.json._
import play.api.libs.functional.syntax._
import play.api.data.validation.ValidationError
// 再利用するために独自の Reads を定義します
// この Reads は、文字列が与えられた文字列と等しくないことを検証します
def notEqualReads[T](v: T)(implicit r: Reads[T]): Reads[T] = Reads.filterNot(ValidationError("validate.error.unexpected.value", v))( _ == v )
def skipReads(implicit r: Reads[String]): Reads[String] = r.map( _.substring(2) )
implicit val creatureReads: Reads[Creature] = (
(__ \ "name").read[String] and
(__ \ "isDead").read[Boolean] and
(__ \ "weight").read[Float] and
(__ \ "email").read(email keepAnd minLength[String](5)) and
(__ \ "favorites").read(
(__ \ "string").read[String]( notEqualReads("ni") andKeep skipReads ) and
(__ \ "number").read[Int]( max(86) or min(875) )
tupled
) and
(__ \ "friends").lazyRead( list[Creature](creatureReads) ) and
(__ \ "social").readNullable[String]
)(Creature)
上記の多くのことは論理的に理解できますが、ちょっと説明してみましょう:
§(__ \ "email").read(email keepAnd minLength[String](5))
- 以前に説明した通り、
(__ \ "email").read(…)
はread
関数の引数に渡されたReads[T]
を、与えられた JsPath(__ \ "email")
に適用します。 email keepAnd minLength[String](5) => Reads[String]
は JsValue を検証する Js バリデータです。- 文字列であること :
email: Reads[String]
なので、ここでは型は指定していません - email フォーマットであること
- 少なくとも 5 文字以上であること (minLength は総称的な
Reads[T]
なので、ここでは厳格に型を指定しています)
- 文字列であること :
- なぜ
email and minLength[String](5)
としないのでしょうか? 以前に説明した通り、これはReads[String]
を期待しているところにBuilder[Reads[(String, String)]]
を生成してしまいます。(<~
としても知られる)keepAnd
演算子は期待通りのことを行います: 両辺の検証を行いますが、成功した場合、左辺の結果のみを保持します。
§notEqualReads("ni") andKeep skipReads
String
型であることは(__ \ "knight").read[String]
から推論されるので、notEqualReads[String]("ni")
と書く必要はありません (Scala の型エンジンの威力です)skipReads
は最初の二文字をスキップする独自の Reads です- (
~>
としても知られる)andKeep
演算子は、シンプルに理解することができます : 左辺と右辺の検証を行い、両方とも成功した場合は右辺の結果のみを保持します。今回の場合、notEqualReads
の結果ではなく、skipReads
の結果のみを保持します。
§max(86) or min(875)
- 説明することは何もありませんよね?
or
は馴染み深いOR
論理演算子以外の何物でもありません
§(__ \ "favorites").read(…)
(__ \ "string").read[String]( notEqualReads("ni") andKeep notEqualReads("swallow") ) and
(__ \ "number").read[Int]( max(86) or min(875) )
tupled
(__ \ "string").read[String](…) and (__ \ "number").read[Int](…) => Builder[Reads[(String, Int)]]
を思い出してくださいtupled
は何を意味するのでしょう?Builder[Reads[(String, Int)]]
は、例えばケースクラスのReads[Creature]
を作るapply
関数と共に使うことができます。しかし、まったく簡単に理解できるtupled
も提供されています : これは Builder を “タプル化” します:Builder[Reads[(A, B)]].tupled => Reads[(A, B)]
- 最終的に
(__ \ "favorites").read(Reads[(String, Int)]
はfavorites
フィールドに期待されている(String, Int)
を検証します
§(__ \ "friend").lazyRead( list[Creature](creatureReads) )
これがこのコードでもっとも複雑な行です。しかし、なぜ複雑なのかは理解できます。friend
は Creature
クラス自身において再帰的なフィールドであり、特別な取り扱いが必要です。
list[Creature](…)
はReads[List[Creature]]
を作りますlist[Creature](creatureReads)
は再帰的であり、Scala がこれを解決するために必要なので、明示的にcreatureReads
を引数として渡しています。難し過ぎることはありません…(__ \ "friend").lazyRead[A](r: => Reads[A]))
:lazyRead
は、再帰的な構造を受け入れるために、Reads[A]
の値が 名前渡し であることを期待します。これが、このとても特別な再帰的なケースにおいて頭に入れておかなければならない唯一の工夫です。
§(__ \ "social").readNullable[String]
理解するのに複雑なところは何もありません: option を読み込む必要があり、readNullable
はこれを手助けしてくれます。
これで、この Reads[Creature]
を使うことができます
import play.api.libs.json._
import play.api.libs.functional.syntax._
val gizmojs = Json.obj(
"name" -> "gremlins",
"isDead" -> false,
"weight" -> 1.0F,
"email" -> "[email protected]",
"favorites" -> Json.obj("string" -> "alpha", "number" -> 85),
"friends" -> Json.arr(),
"social" -> "@gizmo"
)
scala> val gizmo = gizmojs.validate[Creature]
gizmo: play.api.libs.json.JsResult[Creature] = JsSuccess(Creature(gremlins,false,1.0,[email protected],(pha,85),List(),Some(@gizmo)),)
val shaunjs = Json.obj(
"name" -> "zombie",
"isDead" -> true,
"weight" -> 100.0F,
"email" -> "[email protected]",
"favorites" -> Json.obj("string" -> "brain", "number" -> 2),
"friends" -> Json.arr( gizmojs))
scala> val shaun = shaunjs.validate[Creature]
shaun: play.api.libs.json.JsResult[Creature] = JsSuccess(Creature(zombie,true,100.0,[email protected],(ain,2),List(Creature(gremlins,false,1.0,[email protected],(alpha,85),List(),Some(@gizmo))),None),)
val errorjs = Json.obj(
"name" -> "gremlins",
"isDead" -> false,
"weight" -> 1.0F,
"email" -> "rrhh",
"favorites" -> Json.obj("string" -> "ni", "number" -> 500),
"friends" -> Json.arr()
)
scala> errorjs.validate[Creature]
res0: play.api.libs.json.JsResult[Creature] = JsError(List((/favorites/string,List(ValidationError(validate.error.unexpected.value,WrappedArray(ni)))), (/email,List(ValidationError(validate.error.email,WrappedArray()), ValidationError(validate.error.minlength,WrappedArray(5)))), (/favorites/number,List(ValidationError(validate.error.max,WrappedArray(86)), ValidationError(validate.error.min,WrappedArray(875))))))
§Reads[A] のその他の機能
§(Reads[A] and Reads[B]).tupled: Reads[(A, B)]
これは Reads[TupleX]
を作るのに便利です
(
(__ \ 'field1).read[String] and
(__ \ 'field2).read[Int]
).tupled : Reads[(String, Int)]
インデックスが指定された JsArray と使うこともできます
(
(__(0)).read[String] and
(__(1)).read[Int]
).tupled : Reads[(String, Int)]
§(Reads[A1 <: A] and Reads[A2 <: A]).reduce(implicit reducer: Reducer[A, B]): Reads[B]
JSON のいくつかの部分を読み込んで集約するのに便利です。
これには implicit な Reducer/Monoid が必要です。JsObject
と JsArray
向けのものが提供されています。
以下は、次の段落で登場する Json トランスフォーマーのいくつかの使用例です:
§JsObject を Reduce する (ブランチをコピーして、ひとつの JsObuject に集約する)
(
(__ \ 'field1).json.pickBranch[JsString] and
(__ \ 'field2).json.pickBranch[JsNumber]
).reduce : Reads[JsObject]
§JsArray を Reduce する (リーフをコピーして、ひとつの JsArray に集約する)
(
(__ \ 'field1).json.pick[JsString] and
(__ \ 'field2).json.pick[JsNumber]
).reduce : Reads[JsArray]
§Writes[T] は (コンビネータ以外) 変更なし
§Play2.0.x における Writes
Play2.0.x
では Json Writes[T]
をどのように書かなければならなかった覚えていますか?writes
関数をオーバーライドする必要がありました。
trait Writes[-A] {
self =>
/**
* オブジェクトを JsValue に変換する
*/
def writes(o: A): JsValue
}
Part1 で使ったものと同じシンプルなケースクラスを取り上げましょう:
case class Creature(
name: String,
isDead: Boolean,
weight: Float
)
Play2.0.x
では Writes[Creature]
を以下のように書いていたことでしょう (Play2.0.x には存在していませんでしたが、もう一度紹介するために新しい Json 文法を使っています ;) ):
import play.api.libs.json._
implicit val creatureWrites = new Writes[Creature] {
def writes(c: Creature): JsValue = {
Json.obj(
"name" -> c.name,
"isDead" -> c.isDead,
"weight" -> c.weight
)
}
}
scala> val gizmo = Creature("gremlins", false, 1.0F)
scala> val gizmojs = Json.toJson(gizmo)
gizmojs: play.api.libs.json.JsValue = {"name":"gremlins","isDead":false,"weight":1.0}
§Play2.1.x における Writes
不安に思うことはありません: Play2.1 では、まったく同じ方法で Writes を書きます :D
それでは何が違うのでしょう?
Part1 で述べられた通り、Reads
はシンプルな論理演算子を使って結合することができました。
Scala の関数型の力を使うことで、**Writes[T]
コンビネータを提供** できるようになりました。
この要件に適応するように総称的な関数型の構造に基づいて実装する方法の、より論理的な側面を知りたい場合は、@sadache の書いたこのポスト “アプリカティブは制約し過ぎ。アプリカティブの打破と関数型ビルダの紹介” で読むことができます。
§Writes の主な変更点: コンビネータ
再びコードから始めましょう: 以前の Writes[T]
をコンビネータを使って書き直します。
// 重要。必要なツールをスコープにインポートします
import play.api.libs.json._
// 必要な関数型の総称的な構造をインポートします
import play.api.libs.functional.syntax._
implicit val creatureWrites = (
(__ \ "name").write[String] and
(__ \ "isDead").write[Boolean] and
(__ \ "weight").write[Float]
)(unlift(Creature.unapply))
// あるいは、Scala のパーサコンビネータを知っている人は、これに着想を得た演算子を使います
implicit val creatureWrites = (
(__ \ "name").write[String] ~
(__ \ "isDead").write[Boolean] ~
(__ \ "weight").write[Float]
)(unlift(Creature.unapply))
scala> val c = Creature("gremlins", false, 1.0F)
scala> val js = Json.toJson(c)
js: play.api.libs.json.JsValue = {"name":"gremlins","isDead":false,"weight":1.0}
いくつかの点を除いて、Reads[T]
にとてもよく似ているでしょう?
ちょっと説明してみましょう (Reads の記事をコピーしてちょっとだけ変更して… 面倒くさがりなんです ;)):
§import play.api.libs.json.Writes._
その他のインポートに影響するものを除いて、Writes[T]
に必要なものだけをインポートします。
§(__ \ "name").write[String]
この JsPath に write[String]
を適用します (Reads
とまったく同じです)
§and
は Writes[A] and Writes[B] => Builder[Writes[A ~ B]]
を意味する演算子に過ぎません
A ~ B
はCombine A and B
を意味しますが、どのように結合するかは前提としません (タプル、オブジェクト、その他どんなものにでもなることができます)Builder
は実際の型ではありませんが、and
演算子は直接Writes[A ~ B]
を作るのではなく、Writes[A ~ B]
を作れたり、または別のWrites[C]
と結合できたりする中間成果物を作ることを述べるために紹介しています
§(…)(unlift(Creature.unapply))
は Writes[Creature]
を作ります
- 以下のことに気を付けてください:
(__ \ "name").write[String] and (__ \ "isDead").write[Boolean] and (__ \ "weight").write[Float]`
これは、以下のものを作ります
Builder[Writes[String ~ Boolean ~ Float])]` しかし、欲しいのは `Writes[Creature] です
- そのため、最終的に
Writes[Creature]
を取得するために、関数Creature => (String, Boolean, Float)
にBuilder[Writes[String ~ Boolean ~ String])]
を適用します。Builder[Writes[String ~ Boolean ~ String])]
からWrites[Creature]
を取得するためにCreature => (String, Boolean, Float)
を提供するのは少し奇妙に見えるかもしれませんが、これはWrites[-T]
の反変的な性質によるものであることに注意してください。 Creature.unapply
がありますが、このシグネチャはCreature => Option[(String, Boolean, Float)]
なので、Creature => (String, Boolean, Float)
を取得するためにunlift
します
気に留めておかなければならない唯一のことは、この
unlift
呼び出しが自然でないように見えるのは最初だけということです!
推測されている通り、書き込みの際にはバリデーションを行わず、エラー処理がまったく無いため、Writes[T]
は Reads[T]
よりずっと簡単です。
さらに、このため Writes[T]
に提供されている演算子は Reads[T]
ほど豪華でないことを気に留めておかなければなりません。keepAnd
と andKeep
を覚えていますか? これらは Writes[T]
において何の意味もありません。A~B
を書き込むときは、A and B
を書くのであって、only A or only B
ではありません。このため、and
がただ一つ Writes[T]
に提供されている演算子です。
§複雑化された場合
Part1 で使った、もっと複雑な例に立ち返ってみましょう。
以下のようにモデリングした生き物を想像したことを思い出してください:
case class Creature(
name: String,
isDead: Boolean,
weight: Float,
email: String, // email フォーマットかつ minLength(5)
favorites: (String, Int), // 間抜けなお気に入りデータ
friends: List[Creature] = Nil, // ええ、デフォルトでは友達がいません
social: Option[String] = None // デフォルトでは社交的ではありません
)
これに対応する Writes[Creature]
を書いてみましょう。
// 重要。必要なツールをスコープにインポートします
import play.api.libs.json._
// 必要な関数型の総称的な構造をインポートします
import play.api.libs.json.functional.syntax._
implicit val creatureWrites: Writes[Creature] = (
(__ \ "name").write[String] and
(__ \ "isDead").write[Boolean] and
(__ \ "weight").write[Float] and
(__ \ "email").write[String] and
(__ \ "favorites").write(
(__ \ "string").write[String] and
(__ \ "number").write[Int]
tupled
) and
(__ \ "friends").lazyWrite(Writes.traversableWrites[Creature](creatureWrites)) and
(__ \ "social").write[Option[String]]
)(unlift(Creature.unapply))
val gizmo = Creature("gremlins", false, 1.0F, "[email protected]", ("alpha", 85), List(), Some("@gizmo"))
val gizmojs = Json.toJson(gizmo)
gizmojs: play.api.libs.json.JsValue = {"name":"gremlins","isDead":false,"weight":1.0,"email":"[email protected]","favorites":{"string":"alpha","number":85},"friends":[],"social":"@gizmo"}
val zombie = Creature("zombie", true, 100.0F, "[email protected]", ("ain", 2), List(gizmo), None)
val zombiejs = Json.toJson(zombie)
zombiejs: play.api.libs.json.JsValue = {"name":"zombie","isDead":true,"weight":100.0,"email":"[email protected]","favorites":{"string":"ain","number":2},"friends":[{"name":"gremlins","isDead":false,"weight":1.0,"email":"[email protected]","favorites":{"string":"alpha","number":85},"friends":[],"social":"@gizmo"}],"social":null
とても単純であることが分かると思います。特別な演算子が無いので、Reads[T]
よりはるかに簡単です。
いくつか説明します:
§(__ \ "favorites").write(…)
(__ \ "string").write[String] and
(__ \ "number").write[Int]
tupled
(__ \ "string").write[String](…) and (__ \ "number").write[Int](…) => Builder[Writes[String ~ Int]]
を思い出してくださいtupled
は何を意味するのでしょう?Reads[T]
のときと同様に、これは Builder を “タプル化” します:Builder[Writes[A ~ B]].tupled => Writes[(A, B)]
§(__ \ "friend").lazyWrite(Writes.traversableWrites[Creature](creatureWrites))
これは Creature
クラス自身の再帰的なフィールドを取り扱うための、lazyRead
の対称となるコードです。
Writes.traversableWrites[Creature](creatureWrites)
は、再帰のためにWrites[Creature]
自身を渡してWrites[Traversable[Creature]]
を作ります (間もなくlist[Creature]
が登場するので、覚えておいてください ;))(__ \ "friends").lazyWrite[A](r: => Writes[A]))
:lazyWrite
は、再帰的な構造を受け入れるために、Writes[A]
の値が 名前渡し であることを期待します。これが、このとても特別な再帰的なケースにおいて頭に入れておかなければならない唯一の工夫です。
ちなみに、
Writes.traversableWrites[Creature]: Writes[Traversable[Creature]]
をWrites[List[Creature]]
に置き換えられることを不思議に思うかもしれませんね?
これは、Writes[-T]
が反変的な意味をもつためです。Traversable[Creature]
と書けるのであれば、Traversable
を継承するList
としてList[Creature]
を書くことができます (継承の関連は、反変性によって取り消されます) 。
§Writes[A] その他の機能
§Writes[A].contramap( B => A ): Writes[B]
Writes[A]
は contramap できる反変的な要素です。
このため、Writes[B]
に変換するための関数 B => A
を与えてやらなければなりません。
例:
scala> case class Person(name: String)
defined class Person
scala> __.write[String].contramap( (p: Person) => p.name )
res5: play.api.libs.json.OWrites[Person] = play.api.libs.json.OWrites$$anon$2@61df9fa8
§(Writes[A] and Writes[B]).tupled: Writes[(A, B)]
Writes[TupleX]
を作るのに便利です。
(
(__ \ 'field1).write[String] and
(__ \ 'field2).write[Int]
).tupled : Writes[(String, Int)]
既知の制限 以下は動作しないことに気を付けてください: Write コンビネータ は JsObject の生成方法しか知らず、JsArray の生成方法は知らないため、以下はコンパイルされますが、実行時に落ちます。
// 注意: コンパイルされますが、実行時に落ちます
(
(__(0)).write[String] and
(__(1)).write[Int]
).tupled : Writes[(String, Int)]
§(Writes[A1 <: A] and Writes[A2 <: A]).join: Writes[A]
複数のブランチに同じ値を書き込むのに便利です。
例えば:
// Write の結果の型を与えなければならないことに注意してください
scala> val jsWrites: Writes[JsString] = (
| (__ \ 'field1).write[JsString] and
| (__ \ 'field2).write[JsString]
| ).join
jsWrites: play.api.libs.json.Writes[play.api.libs.json.JsString] = play.api.libs.json.OWrites$$anon$2@732db69a
scala> jsWrites.writes(JsString("toto"))
res3: play.api.libs.json.JsObject = {"field1":"toto","field2":"toto"}
§Format 用のコンビネータについて
Play2.1 には、Format[T] extends Reads[T] with Writes[T]
と呼ばれる機能がありました。
これは、同じ場所にシリアライズ/デシリアライズを提供するために、Reads[T]
と Writes[T]
をまとめてミックスしました。
Play2.1 は Reads[T]
と Writes[T]
のコンビネータを提供します。Format[T]
のコンビネータについてはどうでしょう?
とてもシンプルな例に立ち返りましょう:
case class Creature(
name: String,
isDead: Boolean,
weight: Float
)
Reads[Creature]
は、以下のように書きます:
import play.api.libs.json._
import play.api.libs.functional.syntax._
val creatureReads = (
(__ \ "name").read[String] and
(__ \ "isDead").read[Boolean] and
(__ \ "weight").read[Float]
)(Creature)
implicit
を使っていないので、Format[T]
を定義する際はコンテクスト中に暗黙のReads[Creature]
が存在しないことに気を付けてください
Writes[Creature]
は、以下のように書きます:
import play.api.libs.json._
import play.api.libs.functional.syntax._
val creatureWrites = (
(__ \ "name").write[String] and
(__ \ "isDead").write[Boolean] and
(__ \ "weight").write[Float]
)(unlift(Creature.unapply))
§Format[Creature]
を作るために Reads/Writes 両方をかき集めるには?
§ひとつ目の方法 = 既存の reads/writes から作る
以下のようにして、既存の Reads[T]
と Writes[T]
を再利用して Format[T]
を作ることができます:
implicit val creatureFormat = Format(creatureReads, creatureWrites)
val gizmojs = Json.obj(
"name" -> "gremlins",
"isDead" -> false,
"weight" -> 1.0F
)
val gizmo = Creature("gremlins", false, 1.0F)
assert(Json.fromJson[Creature](gizmojs).get == gizmo)
assert(Json.toJson(gizmo) == gizmojs)
§ふたつ目の方法 = コンビネータを使って作る
Reads と Writes のコンビネータを手に入れましたよね?
Play2.1 は関数型プログラミングの魔法による Format コンビネータ も提供します (本当は魔法ではなく、純粋な関数型プログラミングです ;))
いつもどおり、コードから始めます:
import play.api.libs.json._
import play.api.libs.functional.syntax._
implicit val creatureFormat = (
(__ \ "name").format[String] and
(__ \ "isDead").format[Boolean] and
(__ \ "weight").format[Float]
)(Creature.apply, unlift(Creature.unapply))
val gizmojs = Json.obj(
"name" -> "gremlins",
"isDead" -> false,
"weight" -> 1.0F
)
val gizmo = Creature("gremlins", false, 1.0F)
assert(Json.fromJson[Creature](gizmojs).get == gizmo)
assert(Json.toJson(gizmo) == gizmojs)
とくに奇妙なところはありません…
§(__ \ "name").format[String]
これは、与えられた JsPath
で読み/書きを行う format[String] を作ります
§( )(Creature.apply, unlift(Creature.unapply))
Scala の構造にマッピングするために:
Reads[Creature]
には、関数(String, Boolean, Float) => Creature
が必要ですWrites[Creature]
には、関数Creature => (String, Boolean, Float)
が必要です
このため、Format[Creature] extends Reads[Creature] with Writes[Creature]
のように、Creature.apply
と unlift(Creature.unapply)
を用意しました。これでおしまいです…
§もっと複雑な場合
先の例は、構造がとてもシンプルだし、読み/書きが対照的だったため、すこし間が抜けています。以下の例を考えます:
Json.fromJson[Creature](Json.toJson(creature)) == creature
この場合、書き出したいものを読み込みます。逆もまた同様です。このため、Reads[T]
と Writes[T]
の両方をまとめて作る、とてもシンプルな JsPath.format[T]
関数を使うことができます。
しかし、よくあるもっと複雑なケースクラスを取り扱う場合は、どのように Format[T]
を書くのでしょうか?
以下のコードを思い出してください:
import play.api.libs.json._
import play.api.libs.functional.syntax._
// ケースクラス
case class Creature(
name: String,
isDead: Boolean,
weight: Float,
email: String, // email フォーマットかつ minLength(5)
favorites: (String, Int), // 間抜けなお気に入りデータ
friends: List[Creature] = Nil, // ええ、デフォルトでは友達がいません
social: Option[String] = None // デフォルトでは社交的ではありません
)
import play.api.data.validation.ValidationError
import play.api.libs.json.Reads._
// 再利用するために独自の Reads を定義します
// この Reads は、文字列が与えられた文字列と等しくないことを検証します
def notEqualReads[T](v: T)(implicit r: Reads[T]): Reads[T] = Reads.filterNot(ValidationError("validate.error.unexpected.value", v))( _ == v )
def skipReads(implicit r: Reads[String]): Reads[String] = r.map( _.substring(2) )
val creatureReads: Reads[Creature] = (
(__ \ "name").read[String] and
(__ \ "isDead").read[Boolean] and
(__ \ "weight").read[Float] and
(__ \ "email").read(email keepAnd minLength[String](5)) and
(__ \ "favorites").read(
(__ \ "string").read[String]( notEqualReads("ni") andKeep skipReads ) and
(__ \ "number").read[Int]( max(86) or min(875) )
tupled
) and
(__ \ "friends").lazyRead( list[Creature](creatureReads) ) and
(__ \ "social").read(optional[String])
)(Creature)
import play.api.libs.json.Writes._
val creatureWrites: Writes[Creature] = (
(__ \ "name").write[String] and
(__ \ "isDead").write[Boolean] and
(__ \ "weight").write[Float] and
(__ \ "email").write[String] and
(__ \ "favorites").write(
(__ \ "string").write[String] and
(__ \ "number").write[Int]
tupled
) and
(__ \ "friends").lazyWrite(Writes.traversableWrites[Creature](creatureWrites)) and
(__ \ "social").write[Option[String]]
)(unlift(Creature.unapply))
見ての通り、creatureReads
と creatureWrites
は完全に対称的ではないので、以前に行ったようにひとつの Format[Creature]
にまとめることはできません。
Json.fromJson[Creature](Json.toJson(creature)) != creature
願わくば、先に行ったように Reads[T]
と Writes[T]
から Format[T]
を作りたいものです。
import play.api.libs.json._
import play.api.libs.functional.syntax._
implicit val creatureFormat: Format[Creature] = Format(creatureReads, creatureWrites)
// Creature から Json へのシリアライズをテストする
val gizmo = Creature("gremlins", false, 1.0F, "[email protected]", ("alpha", 85), List(), Some("@gizmo"))
val zombie = Creature("zombie", true, 100.0F, "[email protected]", ("ain", 2), List(gizmo), None)
val zombiejs = Json.obj(
"name" -> "zombie",
"isDead" -> true,
"weight" -> 100.0,
"email" -> "[email protected]",
"favorites" -> Json.obj(
"string" -> "ain",
"number" -> 2
),
"friends" -> Json.arr(
Json.obj(
"name" -> "gremlins",
"isDead" -> false,
"weight" -> 1.0,
"email" -> "[email protected]",
"favorites" -> Json.obj(
"string" -> "alpha",
"number" -> 85
),
"friends" -> Json.arr(),
"social" -> "@gizmo"
)
),
"social" -> JsNull
)
assert(Json.toJson(zombie) == zombiejs)
// JSON から Creature へのデシリアライズをテストする (非対称的な読み込みであることに注意してください)
val gizmo2 = Creature("gremlins", false, 1.0F, "[email protected]", ("pha", 85), List(), Some("@gizmo"))
val zombie2 = Creature("zombie", true, 100.0F, "[email protected]", ("n", 2), List(gizmo2), None)
assert(Json.fromJson[Creature](zombiejs).get == zombie2)
§Format[A] のその他の機能
§Format[A].inmap( A => B, B => A ): Format[B]
Format[A]
は共変でもあり反変でもある (つまり不変な) Functor です。
このため、Format[B]
に変換するための関数 A => B
と関数 B => A
の両方を与えてやらなければなりません。
例えば:
scala> case class Person(name: String)
defined class Person
scala> __.format[String].inmap( (name: String) => Person(name), (p: Person) => p.name )
res6: play.api.libs.json.OFormat[Person] = play.api.libs.json.OFormat$$anon$1@2dc083c1
Next: JSON トランスフォーマー