§JSON 讀取/寫入/格式組合器
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
物件(根路徑)來定義 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
JsPath.read[T](implicit r: Reads[T]): Reads[T]
- 建立一個Reads[T]
,它會將隱含引數r
套用到此路徑上的JsValue
。JsPath.readNullable[T](implicit r: Reads[T]): Reads[Option[T]]
- 用於可能遺失或包含 null 值的路徑。
注意:JSON 函式庫提供基本類型的隱含
Reads
,例如String
、Int
、Double
等。
定義個別路徑 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
即可。
其次,使用函式呼叫 CanBuildX
的 apply
方法,將個別值轉換為你的模型,這將傳回你的複雜 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
執行個體或其結果。
map
- 對成功的值進行對應。flatMap
- 將先前的結果轉換為另一個成功的或錯誤的結果。collect
- 篩選(使用樣式比對)並對應成功的值。orElse
- 為異質的 JSON 值指定替代的Reads
。andThen
- 指定另一個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
驗證輔助程式定義自訂驗證規則。以下是常見的一些
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
轉換器用於將某種類型轉換為 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]]
§遞迴類型
我們的範例模型沒有示範的一種特殊情況,就是如何處理遞迴類型的 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[T]
只是 Reads
和 Writes
特質的組合,可以用於隱式轉換,取代其組成部分。
§從 Reads 和 Writes 建立格式
你可以透過從同類型的 Reads
和 Writes
構建 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)
§使用組合器建立格式
如果你的 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)
下一步:JSON 自動對應
在這個文件中發現錯誤?此頁面的原始程式碼可以在 這裡 找到。在閱讀 文件指南 後,請隨時提交拉取請求。有問題或建議要分享嗎?請前往 我們的社群論壇 與社群展開對話。