JSON Reads/Writes/Format Combinators

    这一节使用到的 JsValue 结构以及相应的模型:

    1. case class Location(lat: Double, long: Double)
    2. case class Resident(name: String, age: Int, role: Option[String])
    3. case class Place(name: String, location: Location, residents: Seq[Resident])

    JsPath 是构建Reads/Writes 的核心。JsPath 指明了数据在 JsValue 中的位置。你可以使用 JsPath 对象(根路径)来定义一个 JsPath 子实例,语法与遍历 JsValue 相似:

    1. import play.api.libs.json._
    2. val json = { ... }
    3. // Simple path
    4. val latPath = JsPath \ "location" \ "lat"
    5. // Recursive path
    6. val namesPath = JsPath \\ "name"
    7. // Indexed path
    8. val firstResidentPath = (JsPath \ "residents")(0)

    play.api.libs.json 包中为 JsPath 定义了一个别名:__(两个下划线),如果你喜欢的话,你也可以使用它:

    1. val longPath = __ \ "location" \ "long"

    Reads 转换器用于将 JsValue 转换成其他类型。你可以通过组合与嵌套 Reads 来构造更复杂的 Reads

    你需要导入以下内容来创建 Reads

    1. import play.api.libs.json._ // JSON library
    2. import play.api.libs.json.Reads._ // Custom validation helpers
    3. import play.api.libs.functional.syntax._ // Combinator syntax

    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]] - 在路径可能缺失,或包含一个空值时使用。

    定义具体某一路径的 Reads

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

    复合 Reads

    你可以将多个单路径 Reads 组合成一个复合 Reads,这样就可以转换复杂模型了。

    为了便于理解,我们先将一个组合结果分解成两条语句。首先通过 and 组合子来组合 Reads 对象:

    1. val locationReadsBuilder =
    2. (JsPath \ "lat").read[Double] and
    3. (JsPath \ "long").read[Double]

    上面产生的结果类型为 ‘FunctionalBuilder[Reads]#CanBuild2[Double, Double]’,这只是一个中间结果,它将被用来创建一个复合 Reads

    第二步调用 CanBuildXapply 方法来将单个的结果转换成你的模型,这将返回一个复合 Reads。如果你有一个带有构造器签名的样例类,你就可以直接使用它的 apply 方法:

    1. implicit val locationReads = locationReadsBuilder.apply(Location.apply _)

    将上述代码合成一条语句:

    1. implicit val locationReads: Reads[Location] = (
    2. (JsPath \ "lat").read[Double] and
    3. (JsPath \ "long").read[Double]
    4. )(Location.apply _)

    方法已经在 一节中介绍过,我们推荐它进行验证以及用它将 JsValue 转换成其它类型。以下是基本用法:

    1. val nameReads: Reads[String] = (JsPath \ "name").read[String]
    2. val nameResult: JsResult[String] = json.validate[String](nameReads)
    3. nameResult match {
    4. case s: JsSuccess[String] => println("Name: " + s.get)
    5. case e: JsError => println("Errors: " + JsError.toFlatJson(e).toString())
    6. }

    Reads 的默认验证比较简单,比如只检查类型转换错误。你可以通过使用 Reads 的验证 helper 来自定义验证规则。以下是一些常用的 helper:

    • 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 - 执行逻辑或并保留最后选中的 Reads 的运算符。

    想添加验证,只需将相应 helper 作为 JsPath.read 的参数即可:

    1. val improvedNameReads =
    2. (JsPath \ "name").read[String](minLength[String](2))

    将它们合起来

    注意,复合 Reads 可以嵌套使用。在上面的例子中,placeReads 使用了前面定义的隐式 locationReadsresidentReads

    Writes 用于将其它类型转换成 JsValue

    为样例类构建简单的 Writes,只需在 Writes 的 apply 方法体中使用一个函数即可:

    1. case class DisplayName(name:String)
    2. implicit val displayNameWrite: Writes[DisplayName] = Writes {
    3. (displayName: DisplayName) => JsString(displayName.name)
    4. }

    你可以使用 JsPath 和组合子来构建复合 Writes,这一点与 Reads 非常相似。以下是我们模型的 Writes

    1. import play.api.libs.json._
    2. import play.api.libs.functional.syntax._
    3. implicit val locationWrites: Writes[Location] = (
    4. (JsPath \ "lat").write[Double] and
    5. (JsPath \ "long").write[Double]
    6. )(unlift(Location.unapply))
    7. implicit val residentWrites: Writes[Resident] = (
    8. (JsPath \ "name").write[String] and
    9. (JsPath \ "age").write[Int] and
    10. (JsPath \ "role").writeNullable[String]
    11. )(unlift(Resident.unapply))
    12. implicit val placeWrites: Writes[Place] = (
    13. (JsPath \ "name").write[String] and
    14. (JsPath \ "location").write[Location] and
    15. (JsPath \ "residents").write[Seq[Resident]]
    16. val place = Place(
    17. "Watership Down",
    18. Location(51.235685, -1.309197),
    19. Seq(
    20. Resident("Fiver", 4, None),
    21. Resident("Bigwig", 6, Some("Owsla"))
    22. )
    23. )
    24. val json = Json.toJson(place)

    复合 Writes 与复合 Reads 有以下不同点:

    • 单个路径的 Writes 通过 JsPath.write 方法来创建。
    • 将模型转换成 JsValue 无需验证,因此也不需要验证 helper。
    • 中间结果 FunctionalBuilder#CanBuildX(由 and 组合子产生)接收一个函数,该函数将复合类型 T 转换成一个元组,该元组与单路径 Writes 匹配。尽管看起来和 Reads 非常对称,样例类的 unapply 方法返回的是属性元组的 Option 类型,因此需要使用 unlift 方法将元组提取出来。

    有一种特殊的情况是上面的例子没有讲到的,即如何处理递归类型的 ReadsWritesJsPath 提供了 lazyReadlazyWrite 方法来处理这种情况:

    1. case class User(name: String, friends: Seq[User])
    2. implicit lazy val userReads: Reads[User] = (
    3. (__ \ "name").read[String] and
    4. (__ \ "friends").lazyRead(Reads.seq[User](userReads))
    5. )(User)
    6. implicit lazy val userWrites: Writes[User] = (
    7. (__ \ "name").write[String] and
    8. (__ \ "friends").lazyWrite(Writes.seq[User](userWrites))
    9. )(unlift(User.unapply))

    Format[T]ReadsWrites 组合的特性(trait),它可以代替 ReadsWrites 来做隐式转换。

    你可以通过 ReadsWrites 来构建针对同一类型的 Format

    1. val locationReads: Reads[Location] = (
    2. (JsPath \ "lat").read[Double](min(-90.0) keepAnd max(90.0)) and
    3. (JsPath \ "long").read[Double](min(-180.0) keepAnd max(180.0))
    4. )(Location.apply _)
    5. val locationWrites: Writes[Location] = (
    6. (JsPath \ "lat").write[Double] and
    7. (JsPath \ "long").write[Double]
    8. )(unlift(Location.unapply))
    9. implicit val locationFormat: Format[Location] =
    10. Format(locationReads, locationWrites)

    使用组合子创建 Format

    1. implicit val locationFormat: Format[Location] = (
    2. (JsPath \ "lat").format[Double](min(-90.0) keepAnd max(90.0)) and
    3. (JsPath \ "long").format[Double](min(-180.0) keepAnd max(180.0))