文档

§JSON 变换器

请注意,此文档最初由 Pascal Voitot (@mandubian) 在 mandubian.com 上发表的文章中发布。

现在您应该知道如何验证 JSON 并将其转换为您可以在 Scala 中编写的任何结构,然后再转换回 JSON。但是,当我开始使用这些组合器来编写 Web 应用程序时,我几乎立即遇到了一个情况:从网络读取 JSON,验证它并将其转换为... JSON。

§介绍 JSON 海岸到海岸 设计

§我们注定要将 JSON 转换为 OO 吗?

几年来,在几乎所有 Web 框架中(也许除了最近的 JavaScript 服务器端内容,其中 JSON 是默认数据结构),我们已经习惯于从网络获取 JSON 并将 JSON(甚至 POST/GET 数据)转换为 OO 结构,例如类(或 Scala 中的 case 类)。为什么?

§OO 转换真的是默认用例吗?

在很多情况下,你并不需要对数据执行任何真正的业务逻辑,而是在存储之前或提取之后进行验证/转换。让我们以 CRUD 为例

因此,通常情况下,对于 CRUD 操作,你将 JSON 转换为 OO 结构,仅仅因为框架只能使用 OO 进行通信。

我并不是说或假装你不应该使用 JSON 到 OO 的转换,但也许这不是最常见的情况,我们应该只在需要执行真正的业务逻辑时才进行 OO 转换。

§新技术玩家改变了操作 JSON 的方式

除了这个事实,我们还有一些新的数据库类型,比如 MongoDB(或 CouchDB),它们接受看起来几乎像 JSON 树的文档结构化数据(_不是 BSON,二进制 JSON 吗?_)。

对于这些数据库类型,我们也有一些很棒的新工具,比如 ReactiveMongo,它提供了一个反应式环境,可以以非常自然的方式将数据流式传输到 Mongo 和从 Mongo 中流式传输数据。

我一直在与 Stephane Godbillon 合作,在编写 Play2-ReactiveMongo 模块 的同时,将 ReactiveMongo 集成到 Play2.1 中。除了 Play2.1 的 Mongo 功能外,该模块还提供 _JSON 到/从 BSON 转换类型类_。

这意味着你可以直接操作从数据库到数据库的 JSON 流,而无需将其转换为 OO。

§JSON _海岸到海岸_ 设计

考虑到这一点,我们可以轻松地想象以下情况

当从数据库提供数据时,情况完全相同

在这种情况下,我们可以轻松地想象**操作从客户端到数据库并返回的 JSON 数据流**,而无需将其显式转换为除 JSON 之外的任何其他内容。
当然,当你将此转换流插入**Play2.1 提供的反应式基础设施**时,它会突然打开新的视野。

这就是我所说的**JSON 海岸到海岸设计**

  • 不要将 JSON 数据视为一个个数据块,而要将其视为**从客户端到数据库(或其他地方)通过服务器的连续数据流**,
  • 将**JSON 流视为一个管道,你可以将其连接到其他管道**,同时应用修改、转换等操作,
  • 以**完全异步/非阻塞**的方式处理流。

这也是 Play2.1 反应式架构存在的理由之一……
我认为从数据流的角度来考虑你的应用程序,会彻底改变你设计网页应用程序的方式。它也可能打开新的功能范围,这些范围比经典架构更适合当今的网页应用程序需求。不过,这不是我们现在要讨论的主题;)

所以,正如你已经自己推断的那样,为了能够直接基于验证和转换来操作 JSON 流,我们需要一些新的工具。JSON 组合器是不错的选择,但它们过于通用。
这就是我们创建了一些专门的组合器和 API,称为JSON 转换器来完成这项工作的原因。

§JSON 转换器是 Reads[T <: JsValue]

请记住,Reads[A <: JsValue] 能够转换,而不仅仅是读取/验证。

§使用 JsValue.transform 而不是 JsValue.validate

我们在 JsValue 中提供了一个函数助手,以帮助人们将 Reads[T] 视为转换器,而不仅仅是验证器。

JsValue.transform[A <: JsValue](reads: Reads[A]): JsResult[A]

这与 JsValue.validate(reads) 完全相同。

§详细信息

在下面的代码示例中,我们将使用以下 JSON

{
  "key1" : "value1",
  "key2" : {
    "key21" : 123,
    "key22" : true,
    "key23" : [ "alpha", "beta", "gamma"],
    "key24" : {
      "key241" : 234.123,
      "key242" : "value242"
    }
  },
  "key3" : 234
}

§案例 1:在 JsPath 中选择 JSON 值

§选择值为 JsValue

import play.api.libs.json._

val jsonTransformer = (__ \ 'key2 \ 'key23).json.pick

scala> json.transform(jsonTransformer)
res9: play.api.libs.json.JsResult[play.api.libs.json.JsValue] = 
  JsSuccess(
    ["alpha","beta","gamma"],
    /key2/key23
  )

§(__ \ 'key2 \ 'key23).json...

§(__ \ 'key2 \ 'key23).json.pick

§JsSuccess(["alpha","beta","gamma"],/key2/key23)

提醒
jsPath.json.pick 只获取 JsPath 内部的值

§将值选取为类型

import play.api.libs.json._

val jsonTransformer = (__ \ 'key2 \ 'key23).json.pick[JsArray]

scala> json.transform(jsonTransformer)
res10: play.api.libs.json.JsResult[play.api.libs.json.JsArray] = 
  JsSuccess(
    ["alpha","beta","gamma"],
    /key2/key23
  )

§(__ \ 'key2 \ 'key23).json.pick[JsArray]

提醒
jsPath.json.pick[T <: JsValue] 只提取 JsPath 内部的类型化值。

§情况 2:选取遵循 JsPath 的分支

§将分支选取为 JsValue

import play.api.libs.json._

val jsonTransformer = (__ \ 'key2 \ 'key24 \ 'key241).json.pickBranch

scala> json.transform(jsonTransformer)
res11: play.api.libs.json.JsResult[play.api.libs.json.JsObject] = 
  JsSuccess(
  {
    "key2": {
      "key24":{
        "key241":234.123
      }
    }
  },
  /key2/key24/key241
  )

§(__ \ 'key2 \ 'key23).json.pickBranch

§{"key2":{"key24":{"key242":"value242"}}}

提醒
jsPath.json.pickBranch 提取到 JsPath 的单个分支 + JsPath 内部的值。

§情况 3:将值从输入 JsPath 复制到新的 JsPath

import play.api.libs.json._

val jsonTransformer = (__ \ 'key25 \ 'key251).json.copyFrom( (__ \ 'key2 \ 'key21).json.pick )

scala> json.transform(jsonTransformer)
res12: play.api.libs.json.JsResult[play.api.libs.json.JsObject] 
  JsSuccess( 
    {
      "key25":{
        "key251":123
      }
    },
    /key2/key21
  )

§(__ \ 'key25 \ 'key251).json.copyFrom( reads: Reads[A <: JsValue] )

§{"key25":{"key251":123}}

提醒
jsPath.json.copyFrom(Reads[A <: JsValue]) 从输入 JSON 中读取值,并创建一个新的分支,结果作为叶子节点。

§情况 4:复制完整的输入 Json & 更新分支

import play.api.libs.json._

val jsonTransformer = (__ \ 'key2 \ 'key24).json.update( 
  __.read[JsObject].map{ o => o ++ Json.obj( "field243" -> "coucou" ) }
)

scala> json.transform(jsonTransformer)
res13: play.api.libs.json.JsResult[play.api.libs.json.JsObject] = 
  JsSuccess(
    {
      "key1":"value1",
      "key2":{
        "key21":123,
        "key22":true,
        "key23":["alpha","beta","gamma"],
        "key24":{
          "key241":234.123,
          "key242":"value242",
          "field243":"coucou"
        }
      },
      "key3":234
    },
  )

§(__ \ 'key2).json.update(reads: Reads[A < JsValue])

§(__ \ 'key2 \ 'key24).json.update(reads) 执行 3 个操作:

§JsSuccess({…},)

提醒
jsPath.json.update(Reads[A <: JsValue]) 仅适用于 JsObject,它会复制完整的输入 JsObject 并使用提供的 Reads[A <: JsValue] 更新 jsPath。

§案例 5:在新的分支中放置给定的值

import play.api.libs.json._

val jsonTransformer = (__ \ 'key24 \ 'key241).json.put(JsNumber(456))

scala> json.transform(jsonTransformer)
res14: play.api.libs.json.JsResult[play.api.libs.json.JsObject] = 
  JsSuccess(
    {
      "key24":{
        "key241":456
      }
    },
  )

§(__ \ 'key24 \ 'key241).json.put( a: => JsValue )

§(__ \ 'key24 \ 'key241).json.put( a: => JsValue )

§jsPath.json.put( a: => JsValue )

§jsPath.json.put

**提醒:**
jsPath.json.put( a: => Jsvalue ) 创建一个带有给定值的新分支,而不考虑输入 JSON。

§案例 6:从输入 JSON 中修剪分支

import play.api.libs.json._

val jsonTransformer = (__ \ 'key2 \ 'key22).json.prune

scala> json.transform(jsonTransformer)
res15: play.api.libs.json.JsResult[play.api.libs.json.JsObject] = 
  JsSuccess(
    {
      "key1":"value1",
      "key3":234,
      "key2":{
        "key21":123,
        "key23":["alpha","beta","gamma"],
        "key24":{
          "key241":234.123,
          "key242":"value242"
        }
      }
    },
    /key2/key22/key22
  )

§(__ \ 'key2 \ 'key22).json.prune

§(__ \ 'key2 \ 'key22).json.prune

请注意,生成的 JsObject 的键顺序与输入 JsObject 的键顺序不同。这是由于 JsObject 的实现和合并机制。但这并不重要,因为我们已经重写了 JsObject.equals 方法来考虑这一点。

提醒
jsPath.json.prune 仅适用于 JsObject,并从输入 JSON 中删除给定的 JsPath。

请注意,
- prune 目前不适用于递归 JsPath
- 如果 prune 没有找到要删除的分支,它不会生成任何错误并返回未更改的 JSON。

§更复杂的案例

§案例 7:选择一个分支并在两个地方更新其内容

import play.api.libs.json._
import play.api.libs.json.Reads._

val jsonTransformer = (__ \ 'key2).json.pickBranch(
  (__ \ 'key21).json.update( 
    of[JsNumber].map{ case JsNumber(nb) => JsNumber(nb + 10) }
  ) andThen 
  (__ \ 'key23).json.update( 
    of[JsArray].map{ case JsArray(arr) => JsArray(arr :+ JsString("delta")) }
  )
)

scala> json.transform(jsonTransformer)
res16: play.api.libs.json.JsResult[play.api.libs.json.JsObject] = 
  JsSuccess(
    {
      "key2":{
        "key21":133,
        "key22":true,
        "key23":["alpha","beta","gamma","delta"],
        "key24":{
          "key241":234.123,
          "key242":"value242"
        }
      }
    },
    /key2
  )

§(__ \ 'key2).json.pickBranch(reads: Reads[A <: JsValue])

§(__ \ 'key21).json.update(reads: Reads[A <: JsValue])

§of[JsNumber]

§of[JsNumber].map{ case JsNumber(nb) => JsNumber(nb + 10) }

§andThen

§of[JsArray].map{ case JsArray(arr) => JsArray(arr :+ JsString("delta")

请注意,结果只是 __ \ 'key2 分支,因为我们只选择了该分支。

§案例 8:选择一个分支并修剪一个子分支

import play.api.libs.json._

val jsonTransformer = (__ \ 'key2).json.pickBranch(
  (__ \ 'key23).json.prune
)

scala> json.transform(jsonTransformer)
res18: play.api.libs.json.JsResult[play.api.libs.json.JsObject] = 
  JsSuccess(
    {
      "key2":{
        "key21":123,
        "key22":true,
        "key24":{
          "key241":234.123,
          "key242":"value242"
        }
      }
    },
    /key2/key23
  )

§(__ \ 'key2).json.pickBranch(reads: Reads[A <: JsValue])

§(__ \ 'key23).json.prune

请注意,结果只是 __ \ 'key2 分支,没有 key23 字段。

§组合器怎么样?

我在这里停下来,以免变得无聊(如果还没有的话)…

请记住,您现在拥有一个强大的工具集来创建通用的 JSON 转换器。您可以将转换器组合、映射、扁平映射在一起,形成其他转换器。因此,可能性几乎是无限的。

但还有一个需要处理的最后一点:将这些强大的新 JSON 转换器与之前介绍的 Reads 组合器混合使用。这非常简单,因为 JSON 转换器只是 Reads[A <: JsValue]

让我们通过编写一个 **Gizmo 到 Gremlin** 的 JSON 转换器来演示。

这是 Gizmo

val gizmo = Json.obj(
  "name" -> "gizmo",
  "description" -> Json.obj(
    "features" -> Json.arr( "hairy", "cute", "gentle"),
    "size" -> 10,
    "sex" -> "undefined",
    "life_expectancy" -> "very old",
    "danger" -> Json.obj(
      "wet" -> "multiplies",
      "feed after midnight" -> "becomes gremlin"
    )
  ),
  "loves" -> "all"
)

这是 Gremlin

val gremlin = Json.obj(
  "name" -> "gremlin",
  "description" -> Json.obj(
    "features" -> Json.arr("skinny", "ugly", "evil"),
    "size" -> 30,
    "sex" -> "undefined",
    "life_expectancy" -> "very old",
    "danger" -> "always"
  ),
  "hates" -> "all"
)

好的,让我们编写一个 JSON 转换器来执行此转换

import play.api.libs.json._
import play.api.libs.json.Reads._
import play.api.libs.functional.syntax._

val gizmo2gremlin = (
  (__ \ 'name).json.put(JsString("gremlin")) and
  (__ \ 'description).json.pickBranch(
    (__ \ 'size).json.update( of[JsNumber].map{ case JsNumber(size) => JsNumber(size * 3) } ) and
    (__ \ 'features).json.put( Json.arr("skinny", "ugly", "evil") ) and
    (__ \ 'danger).json.put(JsString("always"))
    reduce
  ) and
  (__ \ 'hates).json.copyFrom( (__ \ 'loves).json.pick )
) reduce

scala> gizmo.transform(gizmo2gremlin)
res22: play.api.libs.json.JsResult[play.api.libs.json.JsObject] = 
  JsSuccess(
    {
      "name":"gremlin",
      "description":{
        "features":["skinny","ugly","evil"],
        "size":30,
        "sex":"undefined",
        "life_expectancy":
        "very old","danger":"always"
      },
      "hates":"all"
    },
  )

我们在这里 ;)
我不会解释所有这些,因为你现在应该能够理解。
只需注意

§(__ \ 'features).json.put(…)(__ \ 'size).json.update 之后,以便它覆盖原始 (__ \ 'features)

§(Reads[JsObject] and Reads[JsObject]) reduce

下一步:使用 XML


发现此文档中的错误?此页面的源代码可以在 此处 找到。阅读完 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?前往 我们的社区论坛 与社区开始对话。