§JSON Reads/Writes/Format コンビネータ
JSONの基礎では、Reads
とWrites
コンバーターが紹介されました。これらはJsValue
構造と他のデータ型との間で変換するために使用されます。このページでは、これらのコンバーターを構築する方法と、変換中に検証を使用する方法について詳しく説明します。
このページの例では、この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
構造内のデータの場所を表します。JsPath
オブジェクト(ルートパス)を使用して、JsValue
をトラバースするのに似た構文で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
§パス Reads
JsPath
には、指定されたパスにあるJsValue
に別のReads
を適用する特別なReads
を作成するメソッドがあります
JsPath.read[T](implicit r: Reads[T]): Reads[T]
- 暗黙的な引数r
をこのパスのJsValue
に適用するReads[T]
を作成します。JsPath.readNullable[T](implicit r: Reads[T]): Reads[Option[T]]
- 欠落している可能性のあるパスまたはnull値を含めることができるパスに使用します。
注:JSONライブラリは、
String
、Int
、Double
などの基本型に対して暗黙的なReads
を提供します。
個々のパスReads
を定義すると、次のようになります
val nameReads: Reads[String] = (JsPath \ "name").read[String]
§複雑な Reads
play.api.libs.functional.syntax
を使用して個々のパスReads
を組み合わせ、より複雑なモデルへの変換に使用できる、より複雑なReads
を形成できます。
理解を深めるために、組み合わせ機能を2つのステートメントに分解します。最初に、and
コンビネータを使用してReads
オブジェクトを組み合わせます
import play.api.libs.functional.syntax._ // Combinator syntax
val locationReadsBuilder =
(JsPath \ "lat").read[Double] and
(JsPath \ "long").read[Double]
これにより、FunctionalBuilder[Reads]#CanBuild2[Double, Double]
の型が得られます。これは中間オブジェクトであり、あまり気にする必要はありません。複雑なReads
の作成に使用されることを知っておいてください。
次に、CanBuildX
のapply
メソッドを、個々の値をモデルに変換する関数とともに呼び出します。これにより、複雑なReads
が返されます。一致するコンストラクターシグネチャを持つケースクラスがある場合は、そのapply
メソッドを使用できます
implicit val locationReads: Reads[Location] = locationReadsBuilder.apply(Location.apply _)
同じコードを1つのステートメントで次に示します
implicit val locationReads: Reads[Location] = (
(JsPath \ "lat").read[Double] and
(JsPath \ "long").read[Double]
)(Location.apply _)
§Readsの関数型コンビネータ
通常の関数型コンビネータを使用して、Reads
インスタンスまたはその結果を変換できます。
map
- 成功した値をマッピングします。flatMap
- 前の結果を別の成功した結果またはエラーのある結果に変換します。collect
- (パターンマッチングを使用して)成功した値をフィルター処理およびマッピングします。orElse
- 異種JSON値の代替Reads
を指定します。andThen
- 最初の結果を後処理する別のReads
を指定します。
val strReads: Reads[String] = JsPath.read[String]
// .map
val intReads: Reads[Int] = strReads.map { str =>
str.toInt
}
// e.g. reads JsString("123") as 123
// .flatMap
val objReads: Reads[JsObject] = strReads.flatMap { rawJson =>
// consider something like { "foo": "{ \"stringified\": \"json\" }" }
Reads { _ =>
Json.parse(rawJson).validate[JsObject]
}
}
// .collect
val boolReads1: Reads[Boolean] = strReads.collect(JsonValidationError("in.case.it.doesn-t.match")) {
case "no" | "false" | "n" => false
case _ => true
}
// .orElse
val boolReads2: Reads[Boolean] = JsPath.read[Boolean].orElse(boolReads1)
// .andThen
val postprocessing: Reads[Boolean] = Reads[JsBoolean] {
case JsString("no" | "false" | "n") =>
JsSuccess(JsFalse)
case _ => JsSuccess(JsTrue)
}.andThen(JsPath.read[Boolean])
フィルターコンビネータは、Reads
にも適用できます(検証の詳細については、次のセクションを参照してください)。
val positiveIntReads = JsPath.read[Int].filter(_ > 0)
val smallIntReads = positiveIntReads.filterNot(_ > 100)
val positiveIntReadsWithCustomErr = JsPath
.read[Int]
.filter(JsonValidationError("error.positive-int.expected"))(_ > 0)
いくつかの特定のコンビネータは、(.andThen
コンビネータとは対照的に)読み取り前にJSONを処理するために使用できます。
// .composeWith
val preprocessing1: Reads[Boolean] =
JsPath
.read[Boolean]
.composeWith(Reads[JsBoolean] {
case JsString("no" | "false" | "n") =>
JsSuccess(JsFalse)
case _ => JsSuccess(JsTrue)
})
val preprocessing2: Reads[Boolean] = JsPath.read[Boolean].preprocess {
case JsString("no" | "false" | "n") =>
JsFalse
case _ => JsTrue
}
§Readsによる検証
JSONの基礎で、JsValue
から別の型への検証と変換を実行するための推奨方法として、JsValue.validate
メソッドが導入されました。基本的なパターンは次のとおりです
val json = { ... }
val nameReads: Reads[String] = (JsPath \ "name").read[String]
val nameResult: JsResult[String] = json.validate[String](nameReads)
nameResult match {
case JsSuccess(nme, _) => println(s"Name: $nme")
case e: JsError => println(s"Errors: ${JsError.toJson(e)}")
}
Reads
のデフォルトの検証は、型変換エラーのチェックなど、最小限です。Reads
検証ヘルパーを使用して、カスタム検証ルールを定義できます。一般的に使用されるものを次に示します
Reads.email
- 文字列がメール形式かどうかを検証します。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 JsSuccess(place, _) => {
val _: Place = place
// 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]
)(l => (l.lat, l.long))
implicit val residentWrites: Writes[Resident] = (
(JsPath \ "name").write[String] and
(JsPath \ "age").write[Int] and
(JsPath \ "role").writeNullable[String]
)(r => (r.name, r.age, r.role))
implicit val placeWrites: Writes[Place] = (
(JsPath \ "name").write[String] and
(JsPath \ "location").write[Location] and
(JsPath \ "residents").write[Seq[Resident]]
)(p => (p.name, p.location, p.residents))
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
で使用する必要があります。
§Writesの関数型コンビネータ
Reads
と同様に、いくつかの関数型コンビネータをWrites
インスタンスで使用して、値をJSONとして書き込む方法を調整できます。
contramap
- 入力値がWrites
に渡される前に、変換を適用します。transform
- 最初のWrites
によって書き込まれたJSONに変換を適用します。narrow
- JSONとして書き込むことができる値の型を制限します。
val plus10Writes: Writes[Int] = implicitly[Writes[Int]].contramap(_ + 10)
val doubleAsObj: Writes[Double] =
implicitly[Writes[Double]].transform { js =>
Json.obj("_double" -> js)
}
val someWrites: Writes[Some[String]] =
implicitly[Writes[Option[String]]].narrow[Some[String]]
§再帰型
例のモデルでは示されていない特別なケースの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.apply _)
implicit lazy val userWrites: Writes[User] = (
(__ \ "name").write[String] and
(__ \ "friends").lazyWrite(Writes.seq[User](userWrites))
)(u => (u.name, u.friends))
§Format
Format[T]
は、Reads
とWrites
トレイトを組み合わせたものであり、コンポーネントの代わりに暗黙的な変換に使用できます。
§ReadsとWritesからFormatを作成する
同じ型のReads
とWrites
から構築することでFormat
を定義できます
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]
)(l => (l.lat, l.long))
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, l => (l.lat, l.long))
Reads
とWrites
の関数型コンビネータと同様に、Format
にも関数型コンビネータが提供されています。
val strFormat = implicitly[Format[String]]
val intFormat: Format[Int] =
strFormat.bimap(_.size, List.fill(_: Int)('?').mkString)
このドキュメントに誤りを見つけましたか?このページのソースコードはこちらにあります。ドキュメントガイドラインを読んだ後、プルリクエストを送っていただけると幸いです。質問や共有したいアドバイスはありますか?コミュニティフォーラムに参加して、コミュニティとの会話を始めましょう。