文档

§JSON Reads/Writes/Format 组合器

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 对象(根路径)通过使用类似于遍历 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

注意:JSON 库为基本类型(如StringIntDouble 等)提供了隐式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

其次,使用一个函数调用CanBuildXapply方法来将单个值转换为您的模型,这将返回您的复杂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实例或其结果。

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 验证助手定义自定义验证规则。以下是一些常用的验证助手

要添加验证,请将助手作为参数应用于 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 在结构中的特定路径使用先前定义的隐式 locationReadsresidentReads

§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)

复杂WritesReads之间存在一些差异。

§带有Writes的函数组合器

对于Reads,一些函数组合器可以用于Writes实例,以调整如何将值写入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]]

§递归类型

我们的示例模型没有演示的一个特殊情况是如何处理递归类型的ReadsWritesJsPath提供lazyReadlazyWrite方法,它们接受按名称调用的参数来处理这种情况。

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]只是ReadsWrites特性的混合,可以用于隐式转换以代替其组件。

§从Reads和Writes创建Format

您可以通过从相同类型的ReadsWrites构造来定义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

在您的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, l => (l.lat, l.long))

ReadsWrites一样,函数组合器在Format上提供。

val strFormat = implicitly[Format[String]]
val intFormat: Format[Int] =
  strFormat.bimap(_.size, List.fill(_: Int)('?').mkString)

下一步: JSON 自动映射


发现文档中的错误?此页面的源代码可以在 这里 找到。阅读完 文档指南 后,请随时贡献拉取请求。有任何问题或建议要分享?前往 我们的社区论坛 与社区进行交流。