§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 结构是“语言本地的”,并允许以无缝的方式根据您的业务逻辑操作数据,同时确保业务逻辑与 Web 层的隔离。
- 出于一个更值得怀疑的原因:**ORM 框架只通过 OO 结构与数据库进行通信**,我们(有点)说服自己别无选择…… 伴随着众所周知的 ORM 的优点和缺点……(这里不讨论这些问题)
§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。
- 转换 JSON 以符合预期的数据库文档结构。
- 直接将 JSON 发送到数据库(或其他地方)。
当从数据库提供数据时,情况完全相同
- 直接从数据库中提取一些数据作为 JSON。
- 过滤/转换此 JSON,以便仅以客户端期望的格式发送必需的数据(例如,你不希望一些安全信息泄露出去)。
- 直接将 JSON 发送到客户端。
在这种情况下,我们可以轻松地想象**操作从客户端到数据库并返回的 JSON 数据流**,而无需将其显式转换为除 JSON 之外的任何其他内容。
当然,当你将此转换流插入**Play2.1 提供的反应式基础设施**时,它会突然打开新的视野。
这就是我所说的**JSON 海岸到海岸设计**
- 不要将 JSON 数据视为一个个数据块,而要将其视为**从客户端到数据库(或其他地方)通过服务器的连续数据流**,
- 将**JSON 流视为一个管道,你可以将其连接到其他管道**,同时应用修改、转换等操作,
- 以**完全异步/非阻塞**的方式处理流。
这也是 Play2.1 反应式架构存在的理由之一……
我认为从数据流的角度来考虑你的应用程序,会彻底改变你设计网页应用程序的方式。它也可能打开新的功能范围,这些范围比经典架构更适合当今的网页应用程序需求。不过,这不是我们现在要讨论的主题;)
所以,正如你已经自己推断的那样,为了能够直接基于验证和转换来操作 JSON 流,我们需要一些新的工具。JSON 组合器是不错的选择,但它们过于通用。
这就是我们创建了一些专门的组合器和 API,称为JSON 转换器来完成这项工作的原因。
§JSON 转换器是 Reads[T <: JsValue]
- 你可能会说 JSON 转换器只是
f:JSON => JSON
。 - 因此,JSON 转换器可以简单地是一个
Writes[A <: JsValue]
。 - 但是,JSON 转换器不仅仅是一个函数:正如我们所说,我们还希望在转换 JSON 的同时对其进行验证。
- 因此,JSON 转换器是一个
Reads[A <: 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...
- 所有 JSON 转换器都在
JsPath.json.
中。
§(__ \ 'key2 \ 'key23).json.pick
pick
是一个Reads[JsValue]
,它在给定的 JsPath 中选择值。这里为["alpha","beta","gamma"]
§JsSuccess(["alpha","beta","gamma"],/key2/key23)
- 这是一个简单的成功
JsResult
。 - 为了信息,
/key2/key23
代表读取数据的 JsPath,但我们并不关心它,它主要由 Play API 用于组合JsResult(s)
。 ["alpha","beta","gamma"]
只是因为我们重写了toString
方法。
提醒
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]
pick[T]
是一个Reads[T <: JsValue]
,它在给定的JsPath
中选取值(在本例中为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
pickBranch
是一个Reads[JsValue]
,它从根节点到给定的JsPath
选取分支。
§{"key2":{"key24":{"key242":"value242"}}}
- 结果是从根节点到给定 JsPath 的分支,包括
JsPath
中的 JsValue。
提醒
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] )
copyFrom
是一个Reads[JsValue]
。copyFrom
使用提供的 Reads[A] 从输入 JSON 中读取 JsValue。copyFrom
将此提取的 JsValue 复制为对应于给定 JsPath 的新分支的叶子节点。
§{"key25":{"key251":123}}
copyFrom
读取值123
。copyFrom
将此值复制到新的分支(__ \ 'key25 \ 'key251)
中。
提醒
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])
- 是一个
Reads[JsObject]
。
§(__ \ 'key2 \ 'key24).json.update(reads)
执行 3 个操作:
- 从输入 JSON 中的 JsPath
(__ \ 'key2 \ 'key24)
提取值。 - 对该相对值应用
reads
,并重新创建一个分支(__ \ 'key2 \ 'key24)
,将reads
的结果作为叶子节点添加。 - 将此分支与完整的输入 JSON 合并,替换现有分支(因此它仅适用于输入
JsObject
,而不适用于其他类型的JsValue
)。
§JsSuccess({…},)
- 仅供参考,这里没有第二个参数作为 JsPath,因为 JSON 操作是从根 JsPath 执行的。
提醒
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 )
- 是一个 Reads[JsObject]
§(__ \ 'key24 \ 'key241).json.put( a: => JsValue )
- 创建一个新的分支
(__ \ 'key24 \ 'key241)
- 将
a
作为此分支的叶子。
§jsPath.json.put( a: => JsValue )
- 接受一个通过名称传递的
JsValue
参数,允许传递闭包。
§jsPath.json.put
- 完全不关心输入 JSON。
- 只需用给定的值替换输入 JSON。
**提醒:**
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
- 是一个
Reads[JsObject]
,仅适用于 JsObject
§(__ \ 'key2 \ 'key22).json.prune
- 从输入 JSON 中删除给定的 JsPath(
key22
在key2
下消失了)
请注意,生成的 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])
- 从输入 JSON 中提取分支
__ \ 'key2
并将reads
应用于该分支的相对叶节点(仅对内容)。
§(__ \ 'key21).json.update(reads: Reads[A <: JsValue])
- 更新
(__ \ 'key21)
分支。
§of[JsNumber]
- 只是一个
Reads[JsNumber]
。 - 从
(__ \ 'key21)
中提取一个 JsNumber。
§of[JsNumber].map{ case JsNumber(nb) => JsNumber(nb + 10) }
- 读取一个 JsNumber(
__ \ 'key21
中的值为 _123_)。 - 使用
Reads[A].map
将其增加 _10_(以不可变的方式)。
§andThen
- 只是两个
Reads[A]
的组合。 - 首先应用第一个读取,然后将结果传递给第二个读取。
§of[JsArray].map{ case JsArray(arr) => JsArray(arr :+ JsString("delta")
- 读取一个 JsArray(
__ \ 'key23
中的值为 _[alpha, beta, gamma]_)。 - 使用
Reads[A].map
将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])
- 从输入 JSON 中提取分支
__ \ 'key2
并将reads
应用于该分支的相对叶节点(仅对内容)。
§(__ \ 'key23).json.prune
- 从相对 JSON 中删除分支
__ \ 'key23
。
请注意,结果只是
__ \ '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
- 它合并了两个
Reads[JsObject]
的结果 (JsObject ++ JsObject) - 它还将相同的 JSON 应用于两个
Reads[JsObject]
,这与andThen
不同,后者将第一个读取的结果注入到第二个读取中。
下一步:使用 XML
发现此文档中的错误?此页面的源代码可以在 此处 找到。阅读完 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?前往 我们的社区论坛 与社区开始对话。