理解 JSON Schema 的词法作用域和动态作用域
JSON Schema 组织定义的大多数关键字可以独立评估,或者只考虑相邻关键字的值。例如,type
关键字独立于任何其他关键字,而 additionalProperties
关键字依赖于在同一 schema 对象中定义的 properties
和 patternProperties
关键字。
如果您想了解更多关于关键字依赖关系的信息,请查看由Greg Dennis 编写的JSON Schema 的静态分析 文章。
但是,有一小部分关键字的评估取决于它们所在的作用域。这些关键字是 $ref
、$dynamicRef
、unevaluatedItems
和 unevaluatedProperties
。此外,还有一些关键字影响它们所在的范围。这些关键字是 $id
、$schema
、$anchor
和 $dynamicAnchor
。
JSON Schema 为 URI 解析定义了两种作用域类型:词法作用域和动态作用域。了解这些作用域的工作原理对于掌握 JSON Schema 中一些最先进(也经常令人困惑!)的功能至关重要,例如动态引用。
Schema 资源
在我们深入词法作用域和动态作用域之前,让我们回顾一下一些 JSON Schema 基础知识。
$id
关键字定义了schema 的 URI。虽然此关键字通常在顶层设置,但任何子 schema 都可以声明它来使用不同的 URI 进行区分。例如,以下 schema 定义了 4 个标识符,一些是相对的,一些是绝对的
在 JSON Schema 的术语中,我们说 $id
关键字引入了新的schema 资源,顶层 schema 资源被称为根 schema 资源。
请看以下示例。该 schema 包含 3 个 schema 资源,每个资源使用不同的颜色突出显示:根 schema 资源(红色)、/properties/foo
(蓝色)处的 schema 资源和 /properties/bar
(绿色)处的 schema 资源。请注意,/properties/baz
处的子 schema 是根 schema 资源的一部分,因为它没有引入新的标识符
请注意,子 schema 资源不被认为是父 schema 资源的一部分。例如,在上图中,https://example.com/foo
或 https://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 资源之间关系的有向图表示
正如您将看到的,将 schema 视为 schema 资源的有向图,对于理解词法作用域和动态作用域都有很大帮助。
词法作用域
根据上一节中的图类比,schema 的词法作用域由正在评估的节点组成。换句话说,schema 的词法作用域由它所属的整个 schema 资源组成。
请看以下示例序列。在左侧,一个 JSON Schema,其中包含一个嵌套的 schema 资源。在右侧,名为 https://example.com/person
的根 schema 资源和名为 https://example.com/surname
的嵌套 schema 资源的相应有向图表示。在评估过程的每个步骤中,我们都会将 schema 和有向图中不属于词法作用域的部分灰显。
评估过程从顶层 schema 开始。此时词法作用域是根 schema 资源,嵌套的 schema 资源不在作用域内。
然后,我们进入 properties
应用程序,如果实例定义了 firstName
属性,我们就会进入 /properties/firstName
处的子 schema。该子 schema 是根 schema 资源的一部分(因为它没有声明自己的标识符),因此词法作用域与上一步保持一致。
最后,如果实例定义了 lastName
属性,我们就会遵循 properties
应用程序进入 /properties/lastName
处的子 schema。该子 schema 定义了一个新的 schema 资源,因此此时词法作用域是嵌套的 schema 资源,根 schema 资源不在作用域内。
请注意,根据定义,任何子 schema 的词法作用域都可以通过静态确定来确定,而无需考虑实例,就像我们在这里做的那样。
词法作用域和锚点
作为另一个实际示例,请考虑 $anchor
关键字,它为 schema 定义了一个与位置无关的标识符。该关键字不仅会影响它所在 schema 对象,还会影响它的词法作用域。这就是为什么在同一个 schema 资源中多次声明相同的锚点标识符会发生错误(词法作用域冲突),而可以在不同的 schema 资源上声明相同的锚点标识符(因为词法作用域不同)
遵循引用
当评估过程遇到引用关键字时,它会放弃引用 schema 的词法作用域,并进入目标 schema 的词法作用域。
如果引用指向同一 schema 资源中的一个子 schema,词法作用域将保持不变。回到图类比,每个节点都代表一个 schema 资源,因此评估过程将停留在同一个节点。但是,如果引用指向另一个不同 schema 资源上的子 schema,目标 schema 的 schema 资源将成为新的词法作用域。在图类比中,评估过程会沿着箭头指向另一个节点。
在 Schema 资源内
在以下示例中,位于 /items/$ref
的引用指向 /$defs/person-name
。目标架构是同一架构资源(根架构资源)的一部分,因此词法范围保持不变。
跨架构资源
现在考虑以下示例序列。在左侧,一个名为 https://example.com/point-in-time
的 JSON 架构,其中包含一个嵌套的架构资源(位于 /$defs/timestamp
)和一个指向外部架构的引用,名为 https://example.com/epoch
(来自 /anyOf/1/$ref
)。在右侧,是根架构资源、嵌套架构资源和外部架构资源的相应有向图表示。与之前一样,在评估过程的每个步骤中,我们都会将架构和有向图中不属于词法范围的部分变灰。
评估过程从顶层架构开始。此时,词法范围是根架构资源,嵌套架构资源和外部架构资源都超出范围。
然后,我们进入 anyOf
逻辑应用器的第一个分支,并按照 /anyOf/0/$ref
(以红色突出显示)处的引用进入 /$defs/timestamp
。这个子架构有它自己的标识符,所以词法范围变为嵌套架构资源,而根架构资源和外部架构资源都超出范围。
最后,我们回到根架构资源,进入 anyOf
逻辑应用器的第二个分支,并按照远程引用 /anyOf/1/$ref
(以红色突出显示)进入 https://example.com/epoch
。这个外部架构根据定义是一个单独的架构资源。因此,它成为新的词法范围。这次,根架构资源及其嵌套架构资源都超出范围。
动态范围
回顾一下,架构的词法范围由其封闭的架构资源组成。相比之下,架构的动态范围由迄今为止评估的架构资源堆栈组成。回到我们把架构比作图的类比,动态范围对应于评估过程访问的节点的有序序列。
考虑以下示例序列。在左上角,一个名为 https://example.com/person
的根架构资源,它声明了两个嵌套的架构资源:https://example.com/name
(位于 /properties/name
)和 https://example.com/age
(位于 /properties/age
)。在左下角,一个成功验证架构的示例实例。请注意,该实例没有声明 age
可选属性。在右侧,是这些架构资源之间关系的有向图表示。与我们之前所做的一样,我们将架构和有向图中不属于动态范围的部分变灰。
评估过程从顶层架构开始。此时,动态范围是根架构资源,嵌套架构资源超出范围。到目前为止,词法范围和动态范围是一致的。
由于实例定义了 name
属性,我们进入 properties
应用器进入位于 /properties/name
的子架构。这个子架构引入了新的架构资源。因此,动态范围现在包含 *根架构资源* 和名为 https://example.com/name
的 *嵌套架构资源*,按顺序。
与词法范围相比,架构的动态范围并不总是可以静态确定,因为评估路径通常取决于实例。例如,对于使用 if
或 oneOf
等逻辑应用器关键字的架构,范围内架构资源的有序序列可能会根据实例的特征而有所不同。
遵循引用
到目前为止,我们已经了解到,对于词法范围,按照引用意味着放弃源架构的词法范围并进入目标架构的词法范围。相比之下,对于动态范围,按照指向另一个架构资源的引用意味着 *保留* 当前动态范围并将目标架构资源 *推入* 堆栈的顶部。
在 Schema 资源内
就像词法范围一样,如果引用指向同一架构资源中的子架构,动态范围将保持不变。换句话说,如果目标架构资源与堆栈顶部的架构资源相同,则动态范围不会改变。因此,在评估过程遇到指向另一个架构资源(本地或远程)的引用之前,*词法范围和动态范围是一致的*。
跨架构资源
抛开简单情况,让我们考虑一个跨架构资源包含本地和远程引用的示例。在左上角,一个示例实例和一个名为 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
的外部架构资源的一部分,如左下角所示。在右侧,是这些架构资源和动态范围之间关系的有向图表示。
与迄今为止的其他示例一样,评估过程从顶层架构开始。此时,动态范围是根架构资源,所有其他架构资源都超出范围。
由于实例定义了 name
属性,我们进入 properties
应用器进入位于 /properties/name
的子架构。这个子架构引入了新的架构资源。因此,动态范围现在包含 https://example.com
(根架构资源),然后是 https://example.com/name
(位于 /properties/name
的嵌套架构资源)。
该 https://example.com/name
架构资源引用了另一个嵌套架构资源:https://example.com/person
。按照这个引用后,动态范围现在包含 https://example.com
(根架构资源),然后是 https://example.com/name
(位于 /properties/name
的嵌套架构资源),然后是 https://example.com/person
(位于 /$defs/person
的嵌套架构资源)。
现在是一个有趣的情况。我们目前正在评估名为 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
。
动态范围作为堆栈
在本节开头,我们说架构的动态范围由迄今为止评估的架构资源堆栈组成。但是,我们迄今为止的示例只考虑了将架构资源 *推入* 堆栈的顶部。
在传统的编程语言中,程序执行通常涉及过程调用其他过程,在计算机科学中被称为 调用堆栈。最终,一个过程不会调用任何其他过程。当这样的叶过程完成执行时,调用堆栈将 展开(弹出操作),控制权将返回到调用者帧。
如果您难以理解上一段,您可能会喜欢观看哈佛大学的 调用堆栈 - CS50 短片。
JSON 架构的动态范围的工作方式相同。在某个时刻,一个架构资源不会引用任何其他架构资源。然后,动态范围将展开,从堆栈中弹出最后一个架构资源。
请考虑以下示例序列。在左上角,一个名为 https://example.com/integer
的根模式资源,它使用 if
、then
和 else
逻辑应用器来检查正整数是偶数还是奇数,并生成相应的 title
注解。请注意,每个子模式都是一个单独的模式资源:https://example.com/check
(位于 /if
),https://example.com/even
(位于 /then
),以及 https://example.com/odd
(位于 /else
)。在左下角是偶数整数实例 42。在右边,是这些模式资源与动态作用域之间关系的有向图表示。
如常,评估过程从顶层模式开始。此时,动态作用域是根模式资源,所有其他模式资源都在作用域之外。
接下来,我们进入 if
应用器,它检查整数实例是偶数还是奇数。此子模式声明了一个名为 https://example.com/check
的新模式资源,该资源被压入堆栈。因此,动态作用域由 https://example.com/integer
后跟 https://example.com/check
组成。
嵌套模式资源 https://example.com/check
不引用任何其他模式资源。当评估过程完成并确定实例是偶数整数时,堆栈会展开,模式资源 https://example.com/check
被弹出,评估过程*返回*到根模式资源。因此,动态作用域又变回仅 https://example.com/integer
。
由于 if
子模式成功验证了实例,因此我们进入 then
应用器。此子模式声明了一个名为 https://example.com/even
的新模式资源,该资源被压入堆栈。因此,动态作用域由 https://example.com/integer
后跟 https://example.com/even
组成。
与之前一样,嵌套模式资源 https://example.com/even
不引用任何其他模式资源。因此,评估过程再次返回到根模式资源,动态作用域又变回仅 https://example.com/integer
,评估过程完成。
总结
了解静态和动态作用域如何工作对于深入了解 JSON Schema 至关重要。以下表格总结了需要注意的最重要要点。
比较点 | 词法作用域 | 动态范围 |
---|---|---|
定义 | 由正在评估的模式资源组成 | 由迄今为止评估的模式资源堆栈组成 |
确定作用域 | 无需考虑实例即可静态确定 | 不能总是静态确定。它可能因实例而异 |
遵循引用 | 包括放弃原始模式的词法作用域并进入目标模式的词法作用域 | 包括将目标模式资源压入动态作用域堆栈的顶部 |
在下一篇文章中,我们将基于本文介绍的概念来揭开动态引用($dynamicRef
和 $dynamicAnchor
)的工作原理的神秘面纱。
如果您喜欢此内容并想在数据行业将您的 JSON Schema 技能付诸实践,请查看我的 O'Reilly 书籍:Unifying Business, Data, and Code: Designing Data Products using JSON Schema。您也可以在 LinkedIn 上与我联系。
图片来自 Christina Morillo 的 Pexels。