JSON基础
现代的web应用一般都面临大量的JSON(JavaScript Object Notation)格式的数据操作。为此Play提供了JSON库。
JSON是一种轻量级的数据交换格式,一个典型的JSON结构如下所示:
{
"name" : "Watership Down",
"location" : {
"lat" : 51.235685,
"long" : -1.309197
},
"residents" : [ {
"name" : "Fiver",
"age" : 4,
"role" : null
}, {
"name" : "Bigwig",
"age" : 6,
"role" : "Owsla"
} ]
}
点击这里查看有关JSON的更多信息。
Play的JSON库
play.api.libs.json 包中提供了构造JSON数据的数据结构及转换数据的工具类。此package提供了如下功能:
自动转换:从case class和json相互转换的模板代码。可以从这里入手来快速开始。
自动解析:从request body中解析JSON,遇到异常的JSON格式或者不正确的content-type头时会自动产生错误。
此JSON库也可以脱离Play框架独立使用。只需要在sbt文件中加入:libraryDependencies += “com.typesafe.play” %% “play-json” % playVersion
支持高度的自定义
这个包里边还内置了如下类型:
JsValue
此特质代表一个JSON值。下面的case class都继承了JsValue,分别代表一种有效的JSON type:
你可以使用上面这些JsValue类型构造任意的JSON结构。
Json
Json对象提供了一系列工具方法,主要用途是对JsValue的转换。
JsPath
代表着JsValue中的一个路径,类似XML中的XPath作用。主要用于遍历Jsalue结构和隐式的转换器。
JsValue转换
使用字符串转换Json对象
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"
} ]
}
""")
使用类转换Json
import play.api.libs.json._
val json: JsValue = JsObject(Seq(
"name" -> JsString("Watership Down"),
"location" -> JsObject(Seq("lat" -> JsNumber(51.235685), "long" -> JsNumber(-1.309197))),
"residents" -> JsArray(IndexedSeq(
JsObject(Seq(
"name" -> JsString("Fiver"),
"age" -> JsNumber(4),
"role" -> JsNull
)),
JsObject(Seq(
"name" -> JsString("Bigwig"),
"age" -> JsNumber(6),
"role" -> JsString("Owsla")
))
))
))
Json.obj和Json.arr可以使得构造过程更加容易。注意大多数值不需要显示地使用JsValue来进行包装,工厂方法会自动隐式转换:
import play.api.libs.json.{ JsNull, Json, JsString, JsValue }
val json: JsValue = Json.obj(
"name" -> "Watership Down",
"location" -> Json.obj("lat" -> 51.235685, "long" -> -1.309197),
"residents" -> Json.arr(
Json.obj(
"name" -> "Fiver",
"age" -> 4,
"role" -> JsNull
),
Json.obj(
"name" -> "Bigwig",
"age" -> 6,
"role" -> "Owsla"
)
)
)
writes转换器
转换为JsValue的过程由Json.toJson[T](T)(implicit writes: Writes[T])执行。该功能依赖于隐式的Writes[T]转换器。
Play的JSON API内置了常用类型的转换器,如Int,Double,Sring以及Boolean。也支持对已经支持类型的集合做转换。
import play.api.libs.json._
// basic types
val jsonString = Json.toJson("Fiver")
val jsonNumber = Json.toJson(4)
val jsonBoolean = Json.toJson(false)
// collections of basic types
val jsonArrayOfInts = Json.toJson(Seq(1, 2, 3, 4))
val jsonArrayOfStrings = Json.toJson(List("Fiver", "Bigwig"))
如果要支持自定义的转换器,必须在作用域中隐式提供相应的Writes转换器:
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])
import play.api.libs.json._
implicit val locationWrites = new Writes[Location] {
def writes(location: Location) = Json.obj(
"lat" -> location.lat,
"long" -> location.long
)
}
implicit val residentWrites = new Writes[Resident] {
def writes(resident: Resident) = Json.obj(
"name" -> resident.name,
"age" -> resident.age,
"role" -> resident.role
)
}
implicit val placeWrites = new Writes[Place] {
def writes(place: Place) = Json.obj(
"name" -> place.name,
"location" -> place.location,
"residents" -> place.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)
你也可以使用 combinator 模式:
注意:combinator模式具体介绍请看这里。
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))
遍历JsValue结构体
和操作XML类似,Scala也允许遍历JsValue结构并处理你感兴趣的部分。
注意:以下示例应用于前面示例中创建的JsValue结构。
简单路径 \
将 \ 运算符应用于JsValue将返回与 JsObject 对应的属性,或者JsArray中索引对应的元素:
val lat = (json \ "location" \ "lat").get
// returns JsNumber(51.235685)
val bigwig = (json \ "residents" \ 1).get
// returns {"name":"Bigwig","age":6,"role":"Owsla"}
\ 操作符返回的是一个JsLookupResult,实际内容对应 JsDefined 或者 JsUndefined。可以链式调用 \,只要有一个环节找不到元素就会返回 JsUndefined。在 JsLookupResult 上尝试调用 get 方法将返回查询到的值,如果在 JsUndefined 上调用将会抛出异常。
也可以直接使用 apply 方法来获取数组中对象或索引中的字段(详见下文 直接查询)。与get类似,如果值不存在此方法将抛出异常。
递归路径 \\
使用 \\ 操作符,将在当前对象及其所有子节点中做查询:
val names = json \\ "name"
// returns Seq(JsString("Watership Down"), JsString("Fiver"), JsString("Bigwig"))
直接查询
可以在 JsArray 或者 JsObject 上直接使用 .apply 操作符,它的效果和使用 \ 操作符类似,但是会直接返回获取到的值,而没有用 JsLookupResult 来包装。如果查询不到,会直接抛出异常。
val name = json("name")
// returns JsString("Watership Down")
val bigwig2 = json("residents")(1)
// returns {"name":"Bigwig","age":6,"role":"Owsla"}
// (json("residents")(3)
// throws an IndexOutOfBoundsException
// json("bogus")
// throws a NoSuchElementException
在某些场景下,访问一些已知存在的JSON值,例如在一次性脚本或REPL中,这将非常有用。
JsValue到字符串
字符串转换
val minifiedString: String = Json.stringify(json)
// {"name":"Watership Down","location":{"lat":51.235685,"long":-1.309197},"residents":[{"name":"Fiver","age":4,"role":null},{"name":"Bigwig","age":6,"role":"Owsla"}]}
下面是可读性更好的写法:
val readableString: String = Json.prettyPrint(json)
{
"name" : "Watership Down",
"location" : {
"lat" : 51.235685,
"long" : -1.309197
},
"residents" : [ {
"name" : "Fiver",
"age" : 4,
"role" : null
}, {
"name" : "Bigwig",
"age" : 6,
"role" : "Owsla"
} ]
}
使用 JsValue.as/ asOpt
将JsValue转换为另一种类型的最简单方法是使用JsValue.as[T](implicit fjs: Reads[T]): T。这需要一个类型为Reads[T]的隐式转换器将JsValue转换为T(与Writes[T]正好相反)。与Writes一样,JSON API提供了对基本类型的内置支持。
val name = (json \ "name").as[String]
// "Watership Down"
val names = (json \\ "name").map(_.as[String])
// Seq("Watership Down", "Fiver", "Bigwig")
如果碰到找不到路径或其它无法转换的情况,as 方法将抛出 JsResultException。更安全的方法是 JsValue.asOpt[T](implicit fjs: Reads[T]): Option[T]。
val nameOption = (json \ "name").asOpt[String]
// Some("Watership Down")
val bogusOption = (json \ "bogus").asOpt[String]
// None
虽然 asOpt 方法更加安全,但是会丢失错误信息。
校验
将 JsValue 转换为另一种类型的最佳方式是使用 validate 方法(它接受read类型的参数)。validate 将同时执行校验和转换,最后返回 JsResult 类型。JsResult 由两个类实现:
JsSuccess:转换成功,并包装了正确的result。
JsError:转换失败,包装了一系列的错误信息。
可以使用各种模式来处理校验的结果:
val json = { ... }
val nameResult: JsResult[String] = (json \ "name").validate[String]
// Pattern matching
nameResult match {
case s: JsSuccess[String] => println("Name: " + s.get)
case e: JsError => println("Errors: " + JsError.toJson(e).toString())
}
// Fallback value
val nameOrFallback = nameResult.getOrElse("Undefined")
// map
val nameUpperResult: JsResult[String] = nameResult.map(_.toUpperCase())
// fold
val nameOption: Option[String] = nameResult.fold(
invalid = {
fieldErrors =>
fieldErrors.foreach(x => {
println("field: " + x._1 + ", errors: " + x._2)
})
None
},
valid = {
name => Some(name)
}
)
JsValue到模型
如果需要将JsValue转换为model,必须在作用域中提供相应的 Reads[T] 方法,T对应着你的 model。
注意:自定义校验及转换的 Reads 实现可以参考这里。
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])
import play.api.libs.json._
import play.api.libs.functional.syntax._
implicit val locationReads: Reads[Location] = (
(JsPath \ "lat").read[Double] and
(JsPath \ "long").read[Double]
)(Location.apply _)
implicit val residentReads: Reads[Resident] = (
(JsPath \ "name").read[String] and
(JsPath \ "age").read[Int] and
(JsPath \ "role").readNullable[String]
)(Resident.apply _)
implicit val placeReads: Reads[Place] = (
(JsPath \ "name").read[String] and
(JsPath \ "location").read[Location] and
(JsPath \ "residents").read[Seq[Resident]]
)(Place.apply _)
val json = { ... }
val placeResult: JsResult[Place] = json.validate[Place]
// JsSuccess(Place(...),)
val residentResult: JsResult[Resident] = (json \ "residents")(1).validate[Resident]
// JsSuccess(Resident(Bigwig,6,Some(Owsla)),)