§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
§路径读取
JsPath
有方法可以创建特殊的Reads
,这些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]]
- 用于可能丢失或可能包含空值的路径。
注意:JSON 库为基本类型(如
String
、Int
、Double
等)提供了隐式Reads
。
定义单个路径Reads
如下所示
val nameReads: Reads[String] = (JsPath \ "name").read[String]
§复杂读取
您可以使用play.api.libs.functional.syntax
组合单个路径Reads
,以形成更复杂的Reads
,这些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
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创建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)
下一步: JSON 自动映射
发现文档中的错误?此页面的源代码可以在 这里 找到。阅读完 文档指南 后,请随时贡献拉取请求。有任何问题或建议要分享?前往 我们的社区论坛 与社区进行交流。