§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)このドキュメントに誤りを見つけましたか?このページのソースコードはこちらにあります。ドキュメントガイドラインを読んだ後、プルリクエストを送っていただけると幸いです。質問や共有したいアドバイスはありますか?コミュニティフォーラムに参加して、コミュニティとの会話を始めましょう。