ドキュメント

§JSON Reads/Writes/Format コンビネータ

JSONの基礎では、ReadsWritesコンバーターが紹介されました。これらは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を作成するメソッドがあります

注:JSONライブラリは、StringIntDoubleなどの基本型に対して暗黙的な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の作成に使用されることを知っておいてください。

次に、CanBuildXapplyメソッドを、個々の値をモデルに変換する関数とともに呼び出します。これにより、複雑な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インスタンスまたはその結果を変換できます。

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検証ヘルパーを使用して、カスタム検証ルールを定義できます。一般的に使用されるものを次に示します

検証を追加するには、ヘルパーを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は、構造内の特定のパスで、以前に定義された暗黙的なlocationReadsresidentReadsを使用します。

§Writes

Writesコンバーターは、ある型からJsValueに変換するために使用されます。

JsPathReadsと非常によく似たコンビネータを使用して、複雑な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)

複雑なWritesReadsの間には、いくつかの違いがあります

§Writesの関数型コンビネータ

Readsと同様に、いくつかの関数型コンビネータをWritesインスタンスで使用して、値を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つは、再帰型のReadsWritesを処理する方法です。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]は、ReadsWritesトレイトを組み合わせたものであり、コンポーネントの代わりに暗黙的な変換に使用できます。

§ReadsとWritesからFormatを作成する

同じ型のReadsWritesから構築することで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を作成する

ReadsWritesが対称である場合(実際のアプリケーションではそうでない可能性があります)、コンビネータから直接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))

ReadsWritesの関数型コンビネータと同様に、Formatにも関数型コンビネータが提供されています。

val strFormat = implicitly[Format[String]]
val intFormat: Format[Int] =
  strFormat.bimap(_.size, List.fill(_: Int)('?').mkString)

次:JSON自動マッピング


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