文件

§JSON 讀取/寫入/格式組合器

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 物件(根路徑)來定義 JsPath 子項實例,方法是使用類似於遍歷 JsValue 的語法

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 有方法可以建立特殊的 Reads,將另一個 Reads 套用到指定路徑上的 JsValue

注意:JSON 函式庫提供基本類型的隱含 Reads,例如 StringIntDouble 等。

定義個別路徑 Reads 如下所示

val nameReads: Reads[String] = (JsPath \ "name").read[String]

§複雜的 Reads

你可以結合個別路徑 Reads,使用 play.api.libs.functional.syntax,以形成更複雜的 Reads,可用於轉換為複雜的模型。

為了更容易理解,我們將結合功能分解為兩個陳述。首先使用 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 _)

以下是單一陳述中的相同程式碼

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)

有一些特定的組合器可用於在讀取之前處理 JSON(與 .andThen 組合器相反)。

// .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 進行驗證

JsValue.validate 方法在 JSON 基礎 中介紹,作為從 JsValue 執行驗證和轉換為另一種型別的首選方式。以下是基本模式

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 轉換器用於將某種類型轉換為 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)

複雜的 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]]

§遞迴類型

我們的範例模型沒有示範的一種特殊情況,就是如何處理遞迴類型的 ReadsWritesJsPath 提供 lazyReadlazyWrite 方法,採用呼叫依名稱參數來處理這個問題

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[T] 只是 ReadsWrites 特質的組合,可以用於隱式轉換,取代其組成部分。

§從 Reads 和 Writes 建立格式

你可以透過從同類型的 ReadsWrites 構建 Format 來定義 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)

§使用組合器建立格式

如果你的 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 自動對應


在這個文件中發現錯誤?此頁面的原始程式碼可以在 這裡 找到。在閱讀 文件指南 後,請隨時提交拉取請求。有問題或建議要分享嗎?請前往 我們的社群論壇 與社群展開對話。