§JSON Reads/Writes/Format コンビネータ
JSON の基本 では、JsValue
構造体と他のデータ型との変換に使用されるコンバータ Reads
と Writes
を紹介しました。このページでは、これらのコンバータを構築する方法と、変換中にバリデーションを使用する方法について詳しく説明します。
このページの例では、次のような JsValue
構造体と、対応するモデルを使用します。
import play.api.libs.json._
val json: JsValue = Json.parse("""
{
"name" : "Watership Down",
"location" : {
"lat" : 51.235685,
"long" : -1.309197
},
"residents" : [ {
"name" : "Fiver",
"age" : 4,
"role" : null
}, {
"name" : "Bigwig",
"age" : 6,
"role" : "Owsla"
} ]
}
""")
case class Location(lat: Double, long: Double)
case class Resident(name: String, age: Int, role: Option[String])
case class Place(name: String, location: Location, residents: Seq[Resident])
§JsPath
JsPath
は、Reads
/Writes
を作るための中核となる構成要素です。JsPath
は JsValue
構造体におけるデータの位置を表します。JsValue
を走査する構文と同様のものを使うことにより、(ルートパスの) JsPath
オブジェクトを使って JsPath
子インスタンスを定義できます。
import play.api.libs.json._
val json = { ... }
// Simple path
val latPath = JsPath \ "location" \ "lat"
// Recursive path
val namesPath = JsPath \\ "name"
// Indexed path
val firstResidentPath = (JsPath \ "residents")(0)
play.api.libs.json
パッケージは、JsPath
の別名として __
(ダブルアンダースコア) を定義します。必要に応じてこれを使用できます。
val longPath = __ \ "location" \ "long"
§Reads
Reads
コンバータは JsValue
から別の型に変換するために使われます。Reads
を組み合わせたりネストして、より複雑な Reads
を作成することができます。
Reads
を作るためにこれらのインポートが必要になります。
import play.api.libs.json._ // JSON library
import play.api.libs.json.Reads._ // Custom validation helpers
import play.api.libs.functional.syntax._ // Combinator syntax
§パス Reads
JsPath
には、指定されたパスの JsValue
に、別の Reads
を適用する特殊な Reads
メソッドを作成するメソッドがあります。
JsPath.read[T](implicit r: Reads[T]): Reads[T]
- このパスのJsValue
に暗黙の引数r
を適用するReads[T]
を作成します。JsPath.readNullable[T](implicit r: Reads[T]): Reads[Option[T]]readNullable
- 存在しないか null 値を含む可能性のあるパスに使用します。
メモ: JSON ライブラリは、
String
、Int
、Double
などの基本型に対する暗黙のReads
を提供します。
単一のパスを定義する Reads
は次のようになります。
val nameReads: Reads[String] = (JsPath \ "name").read[String]
§複雑な Reads
複雑なモデルに変換するために使用できる、より複雑な Reads
を形成するために、単一のパス Reads
を組み合わせることができます。
分かりやすくするために、組み合わせた機能を 2 つのステートメントに分解します。最初に、and
コンビネータを使用して Reads
オブジェクトを結合します。
val locationReadsBuilder =
(JsPath \ "lat").read[Double] and
(JsPath \ "long").read[Double]
これは FunctionalBuilder[Reads]#CanBuild2[Double, Double]
の型を生成します。これは中間のオブジェクトであり、複雑な Reads
を作成するために使用されていることだけを知っていれば、あまり心配する必要はありません。
次に、個々の値をモデルに変換する関数を持つ CanBuildX
の apply
メソッドを呼び出すと、複雑な Reads
が返ります。コンストラクタのシグネチャが一致するケースクラスがある場合は、単にその apply
メソッドを使うことができます。
implicit val locationReads = locationReadsBuilder.apply(Location.apply _)
同じコードを単一のステートメントで書くと以下のようになります。
implicit val locationReads: Reads[Location] = (
(JsPath \ "lat").read[Double] and
(JsPath \ "long").read[Double]
)(Location.apply _)
§Reads のバリデーション
JsValue.validate
メソッドは、JsValue
から別の型へのバリデーションと変換を行うための良い方法として JSON の基本 で紹介しました。基本的なパターンは次のとおりです。
val json = { ... }
val nameReads: Reads[String] = (JsPath \ "name").read[String]
val nameResult: JsResult[String] = json.validate[String](nameReads)
nameResult match {
case s: JsSuccess[String] => println("Name: " + s.get)
case e: JsError => println("Errors: " + JsError.toFlatJson(e).toString())
}
Reads
のデフォルトのバリデーションは、型変換エラーをチェックするなどの最小限のものです。Reads
バリデーションヘルパーを使って独自のバリデーションルールを定義することができます。よく使われるものを次に示します。
Reads.email
- E メール形式の文字列を検証します。Reads.minLength(nb)
- 文字列の最小長を検証します。Reads.min
- 数値の最小を検証します。Reads.max
- 数値の最大を検証します。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[A] or Reads[B] => Reads
- 論理 OR を実行し、最後のReads
の結果を保持する演算子。
バリデーションを追加するには、JsPath.read
メソッドの引数としてヘルパーを適用します。
val improvedNameReads =
(JsPath \ "name").read[String](minLength[String](2))
§すべてをひとまとめにする
複雑な Reads
と独自のバリデーションを使うことで、例題モデルに対して有効な Reads
のセットを定義して適用することができます。
import play.api.libs.json._
import play.api.libs.json.Reads._
import play.api.libs.functional.syntax._
implicit val locationReads: Reads[Location] = (
(JsPath \ "lat").read[Double](min(-90.0) keepAnd max(90.0)) and
(JsPath \ "long").read[Double](min(-180.0) keepAnd max(180.0))
)(Location.apply _)
implicit val residentReads: Reads[Resident] = (
(JsPath \ "name").read[String](minLength[String](2)) and
(JsPath \ "age").read[Int](min(0) keepAnd max(150)) and
(JsPath \ "role").readNullable[String]
)(Resident.apply _)
implicit val placeReads: Reads[Place] = (
(JsPath \ "name").read[String](minLength[String](2)) and
(JsPath \ "location").read[Location] and
(JsPath \ "residents").read[Seq[Resident]]
)(Place.apply _)
val json = { ... }
json.validate[Place] match {
case s: JsSuccess[Place] => {
val place: Place = s.get
// do something with place
}
case e: JsError => {
// error handling flow
}
}
複雑な Reads
はネストすることができます。この場合、placeReads
は、あらかじめ定義された暗黙的な locationReads
と residentReads
を構造体内の特定のパスで使います。
§Writes
Writes
コンバータは、ある型を JsValue
に変換するために使われます。
JsPath
と、Reads
にとてもよく似たコンビネータを使って複雑な Writes
を構築することができます。例題モデルの Writes
はこのようになります。
import play.api.libs.json._
import play.api.libs.functional.syntax._
implicit val locationWrites: Writes[Location] = (
(JsPath \ "lat").write[Double] and
(JsPath \ "long").write[Double]
)(unlift(Location.unapply))
implicit val residentWrites: Writes[Resident] = (
(JsPath \ "name").write[String] and
(JsPath \ "age").write[Int] and
(JsPath \ "role").writeNullable[String]
)(unlift(Resident.unapply))
implicit val placeWrites: Writes[Place] = (
(JsPath \ "name").write[String] and
(JsPath \ "location").write[Location] and
(JsPath \ "residents").write[Seq[Resident]]
)(unlift(Place.unapply))
val place = Place(
"Watership Down",
Location(51.235685, -1.309197),
Seq(
Resident("Fiver", 4, None),
Resident("Bigwig", 6, Some("Owsla"))
)
)
val json = Json.toJson(place)
複雑な Writes
と Reads
にはいくつかの違いがあります。
- 個々のパス
Writes
は、JsPath.write
メソッドを使用して作成されます。 JsValue
への変換においてバリデーションは存在しないため、構造が簡単になり、バリデーションヘルパーは必要ありません。- 中間の
FunctionalBuilder#CanBuildX
(and
コンビネータによって生成される) は、複合型T
を個々のパスWrites
に一致するタプルに変換する関数を取ります。これはReads
の場合と対称的ですが、ケースクラスのunapply
メソッドは、プロパティのタプルのOption
を返し、タプルを抽出するにはunlift
と一緒に使わなければなりません。
§再帰型
例題モデルが示していない特殊なケースの 1 つは、再帰型に対して Reads
と Writes
を扱う方法です。JsPath
は、これを処理するための名前による引数を取る lazyRead
メソッドと lazyWrite
メソッドを提供します。
case class User(name: String, friends: Seq[User])
implicit lazy val userReads: Reads[User] = (
(__ \ "name").read[String] and
(__ \ "friends").lazyRead(Reads.seq[User](userReads))
)(User)
implicit lazy val userWrites: Writes[User] = (
(__ \ "name").write[String] and
(__ \ "friends").lazyWrite(Writes.seq[User](userWrites))
)(unlift(User.unapply))
§Format
Format[T]
は Reads
と Writes
の特徴をミックスさせただけのものであり、これらのコンポーネントの代わりに暗黙の変換用に使うことができます。
§Reads と Writes からの Format の作成
Format
は、同じ型の Reads
と Writes
から構築することで定義できます。
val locationReads: Reads[Location] = (
(JsPath \ "lat").read[Double](min(-90.0) keepAnd max(90.0)) and
(JsPath \ "long").read[Double](min(-180.0) keepAnd max(180.0))
)(Location.apply _)
val locationWrites: Writes[Location] = (
(JsPath \ "lat").write[Double] and
(JsPath \ "long").write[Double]
)(unlift(Location.unapply))
implicit val locationFormat: Format[Location] =
Format(locationReads, locationWrites)
§コンビネータを使用した Format の作成
Reads
と Writes
が対称の場合 (実際のアプリケーションではそうでないかもしれません)、コンビネータから直接 Format
を定義することができます。
implicit val locationFormat: Format[Location] = (
(JsPath \ "lat").format[Double](min(-90.0) keepAnd max(90.0)) and
(JsPath \ "long").format[Double](min(-180.0) keepAnd max(180.0))
)(Location.apply, unlift(Location.unapply))
Next: JSON トランスフォーマ
このドキュメントの翻訳は Play チームによってメンテナンスされているものではありません。 間違いを見つけた場合、このページのソースコードを ここ で確認することができます。 ドキュメントガイドライン を読んで、お気軽にプルリクエストを送ってください。