2023 年 8 月 2 日,星期三 ·8分钟阅读

JSON 架构的静态分析

当我第一次实现 JSON 架构时,我采用了一种可能对 C# 开发人员来说很典型的做法:面向对象和过程式编程的结合。然而,在过去的一年左右的时间里,我有一个想法一直萦绕心头。

架构在实例内部的已知位置对值定义了约束。如果我可以以某种方式捕获这些约束并以此方式对架构建模,那该怎么办?然后我只需要做一次那样的工作,当我最终收到一个实例时,我只需要评估每个单独的约束。

我尝试过好几次。我想是四次。每次我离成功都更近了,但我总是遇到一个障碍,无论我用什么设计都无法克服。

但在过去的两周里,我成功了!(而且性能提升非常显著!)

在这篇文章中,我想分享一些我在这个过程中学到的关于 JSON 架构分析的更抽象的东西。 这篇文章是为实现者准备的!

什么是约束?

约束是 JSON 架构的基石。它们是适用于 JSON 数据中特定位置的单个要求。

在这个架构中

数据
{ "type": "object", "properties": { "foo": { "type": "string" }, "bar": { "type": "number" } }}

我们有三个约束

  1. 根实例 必须是一个对象。
  2. 如果存在一个 foo 属性,则其值 必须是字符串。
  3. 如果存在一个 bar 属性,则其值 必须是数字。

每个约束都标识了

  • 我们在架构中的位置,“架构位置”
  • 我们如何到达这里,“评估路径”
  • 实例位置
  • 某个关键字的特定要求

请注意,这些都不实际需要实例,而且我们应该能够预先计算很多内容。

这看起来很容易设置。我们可以对每个约束进行建模,当我们得到一个实例时,我们测试每个约束。如果它们都通过了,则验证就通过了。

简单吧?我也这么认为。它很快就会在很多方面变得复杂起来。

请注意,我没有将这些问题呈现为实现 JSON 架构时需要克服的问题;它们仅仅是存在的机制。

具有依赖关系的关键字

绝大多数关键字都是完全独立运行的。这些关键字就像我们上面看到的 typeproperties 一样,还有 maximumminItemstitleformat 以及其他许多关键字。

但有一些关键字依赖于(或与)其他关键字才能运行。这些关键字是

关键字依赖关系
additionalPropertiesproperties
patternProperties
containsminContains
maxContains
then
else
if
itemsprefixItems
unevaluatedItems*prefixItems
items
unevaluatedItems
unevaluatedProperties*properties
patternProperties
additionalProperties
unevaluatedProperties

* 虽然大多数关键字只能在其兄弟姐妹中找到其依赖关系,但 unevaluated* 关键字也会在其兄弟姐妹的子架构(应用于同一实例位置)中的关键字上产生依赖关系。

if/then/else 关键字是关键字交互的一个很好的例子。

过程式方法将使用类似于以下的分支逻辑

1if ( ... )
2then {
3  ...
4} else {
5  ...
6}

我们评估 if,其结果决定我们是否评估 thenelse

但如果我们将这些视为约束,那么这三个就呈现出一种相当奇特的布尔逻辑。

1valid = (if && then) || (!if && else)

请注意,这与所有单个约束通过意味着验证通过的概念不同。在这种情况下,如果 if 通过,那么 else 是否通过就无关紧要,因为它会被跳过;反之,如果 if 失败,则会跳过 then

虽然 if/then/else 的交互非常容易预先计算,但对于其他一些关键字,例如 additionalProperties,则无法这样做。对于这种情况,您还有额外的复杂情况。

未知实例位置

在最开始,我们将约束定义为应用于特定位置的要求。但是,某些关键字会更普遍地应用其子模式。这些关键字包括

关键字实例位置
patternProperties与其中一个正则表达式键匹配的任何属性
additionalProperties
unevaluatedProperties
unevaluatedItems
未被其中一个依赖项评估的任何对象属性
contains数组中的任何项目
items
unevaluatedItems
未被其中一个依赖项评估的任何数组项目

对于所有这些,您需要实例来确定哪些位置可用。只有这样,您才能完成约束。

这里的策略是构建一个“约束模板”。这个想法是,约束有一个要求,以及一些机制,以便在可用位置已知后确定该要求需要应用到哪里。因此,虽然我们无法构建完整的约束,但我们仍然可以预先完成一部分工作。

静态引用

静态引用,即 $ref,可以在没有实例的情况下提前解析。无论实例如何,它们始终指向同一文档中的同一位置。简单模式。有时。

如果我们有一个递归模式,例如验证链表或二叉树的模式,该怎么办?在这些情况下,递归将在实例不再有任何需要验证的数据时停止,即当您读取列表的末尾或节点上的叶子时。为了处理这种情况,我们可以采用与上一节中相同的“约束模板”方法。

模板解决方案实际上适用于许多情况。真正的诀窍是弄清楚何时需要使用它。

动态引用

另一方面,动态引用通常归结为一件事:动态范围。动态范围是评估进入和离开的资源 ID(通常由 $id 设置)的有序集合。(可以将其视为一个栈,在进入资源时压栈,在离开时出栈。)动态范围受两个因素影响

  • 评估从何处开始
  • 实例数据

根模式动态

去年我写了一篇 文章,描述了如何使用 $dynamicRef 来模拟语言中的泛型类型。这个想法是这样的

  1. 首先,定义一个泛型模式,使用 $dynamicRef 指向 $dynamicAnchor 来标识未定义的“类型”参数。
  2. 定义多个引用泛型模式的辅助模式,并使用它们自己的 $dynamicAnchor 来定义“类型”参数:每个类型一个。

使用这种方法,如果您从泛型模式开始评估,评估将失败,因为“类型”未定义。但是,从辅助模式开始评估会将 $dynamicRef 解析重定向到辅助模式中定义的那个。这种不同的解析可以使实例通过验证。

这是一个动态引用的很好的例子,它 *可以* 在没有实例的情况下解析。您只需要评估的起点。具体来说,您需要知道动态范围从哪里开始,以便识别引用目标。

数据驱动的动态

动态范围改变的另一种方式是通过某种条件逻辑。来自 JSON Schema 测试套件的 这个测试 就是一个很好的例子。它使用了泛型文章中的相同想法,但它没有使用单独的模式,而是将所有内容捆绑到一个模式中。

对于这种情况,根据 kindOfList 实例属性的值,数组中的项目应为数字或字符串。从机制上讲,这是由一组 if/then/else 关键字决定的,这些关键字将评估引导到 numberListstringList 定义中的一个,这两个定义都定义了 $dynamicAnchor: itemType 并引用到包含 $dynamicRef: #itemTypegenericList 定义中。

当最终遇到 $dynamicRef 时,评估必须经过 numberListstringList。这会识别出解析了哪个 $dynamicAnchor

在这种情况下,您无法在没有实例的情况下完全定义约束,因为虽然您可能知道实例位置,但您不知道需要应用哪些要求。

我无法找到任何有效的策略来隔离这种情况的任何预先工作,因此,我仍然必须在评估时计算所有这些内容。

总结

这些是我尝试改变我的模式评估方法时发现的主要问题。JSON Schema 静态分析证明对我来说是一个非常有趣的学习领域,我希望我也激发了您的兴趣。

如果您想了解更多关于我如何最终实现所有这些的信息,我在 blog.json-everything.net 上总结了一下。它现在比过程式更具函数性,但仍然非常面向对象。

这里可能还有很多东西可以探索。如果您想到什么,请随时在 Slack 上找到我。

封面图片由 Google DeepMindUnsplash 上提供