2024 年 2 月 15 日,星期四 ·15分钟阅读

理解 JSON Schema 的词法作用域和动态作用域

JSON Schema 组织定义的大多数关键字可以独立评估,或者只考虑相邻关键字的值。例如,type 关键字独立于任何其他关键字,而 additionalProperties 关键字依赖于在同一 schema 对象中定义的 propertiespatternProperties 关键字。

如果您想了解更多关于关键字依赖关系的信息,请查看由Greg Dennis 编写的JSON Schema 的静态分析 文章。

但是,有一小部分关键字的评估取决于它们所在的作用域。这些关键字是 $ref$dynamicRefunevaluatedItemsunevaluatedProperties。此外,还有一些关键字影响它们所在的范围。这些关键字是 $id$schema$anchor$dynamicAnchor

JSON Schema 为 URI 解析定义了两种作用域类型:词法作用域动态作用域。了解这些作用域的工作原理对于掌握 JSON Schema 中一些最先进(也经常令人困惑!)的功能至关重要,例如动态引用。

Schema 资源

在我们深入词法作用域和动态作用域之前,让我们回顾一下一些 JSON Schema 基础知识。

$id 关键字定义了schema 的 URI。虽然此关键字通常在顶层设置,但任何子 schema 都可以声明它来使用不同的 URI 进行区分。例如,以下 schema 定义了 4 个标识符,一些是相对的,一些是绝对的

A JSON Schema with multiple identifiers

在 JSON Schema 的术语中,我们说 $id 关键字引入了新的schema 资源,顶层 schema 资源被称为根 schema 资源

请看以下示例。该 schema 包含 3 个 schema 资源,每个资源使用不同的颜色突出显示:根 schema 资源(红色)、/properties/foo(蓝色)处的 schema 资源和 /properties/bar(绿色)处的 schema 资源。请注意,/properties/baz 处的子 schema 是根 schema 资源的一部分,因为它没有引入新的标识符

A JSON Schema that consists of 3 schema resources

请注意,子 schema 资源不被认为是父 schema 资源的一部分。例如,在上图中,https://example.com/foohttps://example.com/bar独立的 schema 资源,而不是根 schema 资源的一部分,尽管它们在结构上存在关联。

Schema 作为有向图

JSON Schema 是一种递归数据结构。在 schema 资源的上下文中,这意味着一个 schema 资源可能会引入嵌套的 schema 资源(就像我们在上一节中看到的那样)并使用引用关键字(例如 $ref)来指向外部 schema 资源,从而创建一个 schema 资源的有向图。

请看以下示例。在左上角,一个名为 https://example.com/origin 的根 schema 资源,它声明了一个名为 https://example.com/nested(位于 /properties/bar)的嵌套 schema 资源,并引用了一个名为 https://example.com/destination(来自 /properties/foo/$ref)的外部 schema 资源。在左下角,一个名为 https://example.com/destination 的根 schema 资源,它引用了一个名为 https://example.com/nested-string(来自 /items/$ref)的嵌套 schema 资源。在右侧,这些 schema 资源之间关系的有向图表示

Thinking of a JSON Schema as a directed graph

正如您将看到的,将 schema 视为 schema 资源的有向图,对于理解词法作用域和动态作用域都有很大帮助。

词法作用域

根据上一节中的图类比,schema 的词法作用域由正在评估的节点组成。换句话说,schema 的词法作用域由它所属的整个 schema 资源组成。

请看以下示例序列。在左侧,一个 JSON Schema,其中包含一个嵌套的 schema 资源。在右侧,名为 https://example.com/person 的根 schema 资源和名为 https://example.com/surname 的嵌套 schema 资源的相应有向图表示。在评估过程的每个步骤中,我们都会将 schema 和有向图中不属于词法作用域的部分灰显。

评估过程从顶层 schema 开始。此时词法作用域是根 schema 资源,嵌套的 schema 资源不在作用域内。

The lexical scope of a JSON Schema (1)

然后,我们进入 properties 应用程序,如果实例定义了 firstName 属性,我们就会进入 /properties/firstName 处的子 schema。该子 schema 是根 schema 资源的一部分(因为它没有声明自己的标识符),因此词法作用域与上一步保持一致。

The lexical scope of a JSON Schema (2)

最后,如果实例定义了 lastName 属性,我们就会遵循 properties 应用程序进入 /properties/lastName 处的子 schema。该子 schema 定义了一个新的 schema 资源,因此此时词法作用域是嵌套的 schema 资源,根 schema 资源不在作用域内。

The lexical scope of a JSON Schema (3)

请注意,根据定义,任何子 schema 的词法作用域都可以通过静态确定来确定,而无需考虑实例,就像我们在这里做的那样。

词法作用域和锚点

作为另一个实际示例,请考虑 $anchor 关键字,它为 schema 定义了一个与位置无关的标识符。该关键字不仅会影响它所在 schema 对象,还会影响它的词法作用域。这就是为什么在同一个 schema 资源中多次声明相同的锚点标识符会发生错误(词法作用域冲突),而可以在不同的 schema 资源上声明相同的锚点标识符(因为词法作用域不同)

Example of anchors within and across lexical scopes

遵循引用

当评估过程遇到引用关键字时,它会放弃引用 schema 的词法作用域,并进入目标 schema 的词法作用域。

如果引用指向同一 schema 资源中的一个子 schema,词法作用域将保持不变。回到图类比,每个节点都代表一个 schema 资源,因此评估过程将停留在同一个节点。但是,如果引用指向另一个不同 schema 资源上的子 schema,目标 schema 的 schema 资源将成为新的词法作用域。在图类比中,评估过程会沿着箭头指向另一个节点。

在 Schema 资源内

在以下示例中,位于 /items/$ref 的引用指向 /$defs/person-name。目标架构是同一架构资源(根架构资源)的一部分,因此词法范围保持不变。

Lexical scope after following a reference within the same resource

跨架构资源

现在考虑以下示例序列。在左侧,一个名为 https://example.com/point-in-time 的 JSON 架构,其中包含一个嵌套的架构资源(位于 /$defs/timestamp)和一个指向外部架构的引用,名为 https://example.com/epoch(来自 /anyOf/1/$ref)。在右侧,是根架构资源、嵌套架构资源和外部架构资源的相应有向图表示。与之前一样,在评估过程的每个步骤中,我们都会将架构和有向图中不属于词法范围的部分变灰。

评估过程从顶层架构开始。此时,词法范围是根架构资源,嵌套架构资源和外部架构资源都超出范围。

Lexical scope after following a reference accross resources (1)

然后,我们进入 anyOf 逻辑应用器的第一个分支,并按照 /anyOf/0/$ref(以红色突出显示)处的引用进入 /$defs/timestamp。这个子架构有它自己的标识符,所以词法范围变为嵌套架构资源,而根架构资源和外部架构资源都超出范围。

Lexical scope after following a reference accross resources (2)

最后,我们回到根架构资源,进入 anyOf 逻辑应用器的第二个分支,并按照远程引用 /anyOf/1/$ref(以红色突出显示)进入 https://example.com/epoch。这个外部架构根据定义是一个单独的架构资源。因此,它成为新的词法范围。这次,根架构资源及其嵌套架构资源都超出范围。

Lexical scope after following a reference accross resources (3)

动态范围

回顾一下,架构的词法范围由其封闭的架构资源组成。相比之下,架构的动态范围由迄今为止评估的架构资源堆栈组成。回到我们把架构比作图的类比,动态范围对应于评估过程访问的节点的有序序列。

考虑以下示例序列。在左上角,一个名为 https://example.com/person 的根架构资源,它声明了两个嵌套的架构资源:https://example.com/name(位于 /properties/name)和 https://example.com/age(位于 /properties/age)。在左下角,一个成功验证架构的示例实例。请注意,该实例没有声明 age 可选属性。在右侧,是这些架构资源之间关系的有向图表示。与我们之前所做的一样,我们将架构和有向图中不属于动态范围的部分变灰。

评估过程从顶层架构开始。此时,动态范围是根架构资源,嵌套架构资源超出范围。到目前为止,词法范围和动态范围是一致的。

The dynamic scope of a JSON Schema (1)

由于实例定义了 name 属性,我们进入 properties 应用器进入位于 /properties/name 的子架构。这个子架构引入了新的架构资源。因此,动态范围现在包含 *根架构资源* 和名为 https://example.com/name 的 *嵌套架构资源*,按顺序。

The dynamic scope of a JSON Schema (2)

与词法范围相比,架构的动态范围并不总是可以静态确定,因为评估路径通常取决于实例。例如,对于使用 ifoneOf 等逻辑应用器关键字的架构,范围内架构资源的有序序列可能会根据实例的特征而有所不同。

遵循引用

到目前为止,我们已经了解到,对于词法范围,按照引用意味着放弃源架构的词法范围并进入目标架构的词法范围。相比之下,对于动态范围,按照指向另一个架构资源的引用意味着 *保留* 当前动态范围并将目标架构资源 *推入* 堆栈的顶部。

在 Schema 资源内

就像词法范围一样,如果引用指向同一架构资源中的子架构,动态范围将保持不变。换句话说,如果目标架构资源与堆栈顶部的架构资源相同,则动态范围不会改变。因此,在评估过程遇到指向另一个架构资源(本地或远程)的引用之前,*词法范围和动态范围是一致的*。

Dynamic scope and lexical scopes sometimes align

跨架构资源

抛开简单情况,让我们考虑一个跨架构资源包含本地和远程引用的示例。在左上角,一个示例实例和一个名为 https://example.com 的根架构资源,它声明了两个嵌套的架构资源:https://example.com/name(位于 /properties/name)和 https://example.com/person(位于 /$defs/person),前者引用后者(来自 /properties/name/$ref)。此外,https://example.com/person 引用了一个名为 item 的锚定架构(来自 /$defs/person/$ref),它是名为 https://example.com/people 的外部架构资源的一部分,如左下角所示。在右侧,是这些架构资源和动态范围之间关系的有向图表示。

与迄今为止的其他示例一样,评估过程从顶层架构开始。此时,动态范围是根架构资源,所有其他架构资源都超出范围。

The dynamic scope and remote references (1)

由于实例定义了 name 属性,我们进入 properties 应用器进入位于 /properties/name 的子架构。这个子架构引入了新的架构资源。因此,动态范围现在包含 https://example.com(根架构资源),然后是 https://example.com/name(位于 /properties/name 的嵌套架构资源)。

The dynamic scope and remote references (2)

https://example.com/name 架构资源引用了另一个嵌套架构资源:https://example.com/person。按照这个引用后,动态范围现在包含 https://example.com(根架构资源),然后是 https://example.com/name(位于 /properties/name 的嵌套架构资源),然后是 https://example.com/person(位于 /$defs/person 的嵌套架构资源)。

The dynamic scope and remote references (3)

现在是一个有趣的情况。我们目前正在评估名为 https://example.com/person 的嵌套架构资源。这个架构资源指向名为 https://example.com/people 的远程架构(people#item URI 引用的 people 部分),但不会到达它的根部。相反,它会到达位于 /items 的子架构(people#item URI 引用的 item 锚点所在的位置)。这个子架构是根架构资源的一部分,因此动态范围现在包含 https://example.com(根架构资源),然后是 https://example.com/name(位于 /properties/name 的嵌套架构资源),然后是 https://example.com/person(位于 /$defs/person 的嵌套架构资源),然后是 https://example.com/people

The dynamic scope and remote references (4)

动态范围作为堆栈

在本节开头,我们说架构的动态范围由迄今为止评估的架构资源堆栈组成。但是,我们迄今为止的示例只考虑了将架构资源 *推入* 堆栈的顶部。

在传统的编程语言中,程序执行通常涉及过程调用其他过程,在计算机科学中被称为 调用堆栈。最终,一个过程不会调用任何其他过程。当这样的叶过程完成执行时,调用堆栈将 展开(弹出操作),控制权将返回到调用者帧。

如果您难以理解上一段,您可能会喜欢观看哈佛大学的 调用堆栈 - CS50 短片

JSON 架构的动态范围的工作方式相同。在某个时刻,一个架构资源不会引用任何其他架构资源。然后,动态范围将展开,从堆栈中弹出最后一个架构资源。

请考虑以下示例序列。在左上角,一个名为 https://example.com/integer 的根模式资源,它使用 ifthenelse 逻辑应用器来检查正整数是偶数还是奇数,并生成相应的 title 注解。请注意,每个子模式都是一个单独的模式资源:https://example.com/check(位于 /if),https://example.com/even(位于 /then),以及 https://example.com/odd(位于 /else)。在左下角是偶数整数实例 42。在右边,是这些模式资源与动态作用域之间关系的有向图表示。

如常,评估过程从顶层模式开始。此时,动态作用域是根模式资源,所有其他模式资源都在作用域之外。

The dynamic scope as a stack (1)

接下来,我们进入 if 应用器,它检查整数实例是偶数还是奇数。此子模式声明了一个名为 https://example.com/check 的新模式资源,该资源被压入堆栈。因此,动态作用域由 https://example.com/integer 后跟 https://example.com/check 组成。

The dynamic scope as a stack (2)

嵌套模式资源 https://example.com/check 不引用任何其他模式资源。当评估过程完成并确定实例是偶数整数时,堆栈会展开,模式资源 https://example.com/check 被弹出,评估过程*返回*到根模式资源。因此,动态作用域又变回仅 https://example.com/integer

The dynamic scope as a stack (3)

由于 if 子模式成功验证了实例,因此我们进入 then 应用器。此子模式声明了一个名为 https://example.com/even 的新模式资源,该资源被压入堆栈。因此,动态作用域由 https://example.com/integer 后跟 https://example.com/even 组成。

The dynamic scope as a stack (4)

与之前一样,嵌套模式资源 https://example.com/even 不引用任何其他模式资源。因此,评估过程再次返回到根模式资源,动态作用域又变回仅 https://example.com/integer,评估过程完成。

The dynamic scope as a stack (5)

总结

了解静态和动态作用域如何工作对于深入了解 JSON Schema 至关重要。以下表格总结了需要注意的最重要要点。

比较点词法作用域动态范围
定义由正在评估的模式资源组成由迄今为止评估的模式资源堆栈组成
确定作用域无需考虑实例即可静态确定不能总是静态确定。它可能因实例而异
遵循引用包括放弃原始模式的词法作用域并进入目标模式的词法作用域包括将目标模式资源压入动态作用域堆栈的顶部

在下一篇文章中,我们将基于本文介绍的概念来揭开动态引用($dynamicRef$dynamicAnchor)的工作原理的神秘面纱。

如果您喜欢此内容并想在数据行业将您的 JSON Schema 技能付诸实践,请查看我的 O'Reilly 书籍:Unifying Business, Data, and Code: Designing Data Products using JSON Schema。您也可以在 LinkedIn 上与我联系。

图片来自 Christina MorilloPexels