JSON Reads/Writes/Format 组合子(Combinators)

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 库
import play.api.libs.json.Reads._ // 自定义 验证助手
import play.api.libs.functional.syntax._ // Combinator 语法

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

注意: JSON库为基本类型提供了隐式Reads ,如String, Int, Double, 等。

定义一个具体路径的Reads 如下:

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

复合 Reads

你可以组合单个路径Reads 成复合Reads ,这样可以用来转换复杂模型。

为容易理解, 我们先分解成两条语句。首先使用and 组合子来组合Reads 对象:

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

上面产生的结果类型为FunctionalBuilder[Reads]#CanBuild2[Double, Double]。这是一个中间对象,你不需要担心太多,只需要知道它会被用来创建一个复合Reads

第二步是调用CanBuildXapply 方法,它有一个功能是转换单个值到你的模型, 这会返回你的复合Reads。如果你有一个带有构造器签名的样例类, 你可以只使用它的apply 方法:

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

上述代码合成一条语句:

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

验证 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 s: JsSuccess[String] => println("Name: " + s.get)
  case e: JsError => println("Errors: " + JsError.toFlatJson(e).toString())
}

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 - 执行逻辑或,并保留最后选中的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 s: JsSuccess[Place] => {
    val place: Place = s.get
    // do something with place
  }
  case e: JsError => {
    // error handling flow
  }
}

注意复合Reads 可以嵌套。在本例, placeReads 使用前面定义的隐式locationReadsresidentReads 在结果的特定路径。

Writes

Writes 用于转换一些类型到JsValue

你可以使用和Reads非常类似的JsPath 和组合子构建复合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]
)(unlift(Location.unapply))

implicit val residentWrites: Writes[Resident] = (
  (JsPath \ "name").write[String] and
  (JsPath \ "age").write[Int] and
  (JsPath \ "role").writeNullable[String]
)(unlift(Resident.unapply))

implicit val placeWrites: Writes[Place] = (
  (JsPath \ "name").write[String] and
  (JsPath \ "location").write[Location] and
  (JsPath \ "residents").write[Seq[Resident]]
)(unlift(Place.unapply))


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 是使用JsPath.write 方法创建。
  • 转换到JsValue无需验证,这让结构简单些,并且也不需要任何验证助手。
  • 中间结果FunctionalBuilder#CanBuildX (由and 组合子创建) 接收一个函数为参数,该函数转换复合类型T到一个元组,该元组与单个路径Writes匹配。虽然看起来和Reads 对称, 样例类的unapply 方法返回的是属性元组的Option类型,必须使用unlift 方法将元组提取出来。

递归类型

有一种特殊情况是上面的例子未讲到的,是如何处理递归类型的ReadsWritesJsPath 提供lazyReadlazyWrite 方法,带有call-by-name 参数来处理这种情况:

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)

implicit lazy val userWrites: Writes[User] = (
  (__ \ "name").write[String] and
  (__ \ "friends").lazyWrite(Writes.seq[User](userWrites))
)(unlift(User.unapply))

Format

Format[T] 只是一个ReadsWrites 混合的特质,可以代替这二个进行隐式转换。

从Reads和Writes创建Format

你可以通过Reads and 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]
)(unlift(Location.unapply))

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, unlift(Location.unapply))