本文最初发表在 JSON Schema 博客上,其规范位置为 https://json-schema.org/blog/posts/bundling-json-schema-compound-documents
众所周知,我会说“如果你最近没有重写你的 OpenAPI 捆绑实现,那么你就不支持 OpenAPI 3.1”。这种说法可能是真的,但也许需要更详细的信息?在 oas-kit 中实现对 OAS 3.1 和 JSON Schema 草案 2020-12 的支持时,阅读了 JSON Schema 规范中关于捆绑复合文档的部分,但我仍然不完全清楚符合规范的工具应该怎么做。谢天谢地,Ben Hutton 在这里用一个实例来澄清事实。 – Mike Ralphson,OAI TSC
捆绑的重要性再次提升
OpenAPI 已经将聚光灯聚焦在 JSON Schema 上,而 OpenAPI 3.1 的发布对这两个项目的未来都具有重大意义。我对此感到非常兴奋。
使用 OpenAPI 的平台和库的开发人员之前没有遇到过这样的震动,我的感觉是,可能需要多个版本才能正确实现 JSON Schema 提供的所有新功能。
虽然从 JSON Schema 草案 04 到草案 2020-12 的变化很多,而且博客文章的主题比可能感兴趣的要多,但草案 2020-12 的一个关键“特性”是定义了一个捆绑过程。(草案 04 是 OAS 在 3.1.0 版本之前使用的 JSON Schema 版本;或者说是它的子集/超集。)
事实上,捆绑,如果有什么不同的话,将比以往任何时候都更重要。OAS 3.1 引入对 JSON Schema 的完全支持,极大地增加了使用现有 JSON Schema 文档的开发人员在新的和更新的 OpenAPI 定义中通过引用使用这些文档的可能性。最终的真相来源很重要,它通常是 JSON Schema。
许多工具不支持引用外部资源。捆绑是一种将跨多个文件分布的 schema 资源打包到单个文件中供其他地方使用(例如 OpenAPI 文档)的便捷方式。
现有解决方案?新的解决方案!
有一些库提供了捆绑解决方案,但它们都有局限性,而且我目前还没有看到任何完全了解 JSON Schema 的库。这些库中最受欢迎的是 json-schema-ref-parser,但它报告称它不是为了了解 JSON Schema 而设计的,只是为了涵盖 JSON Reference 规范(现在已经捆绑回 JSON Schema 规范)。
我们希望为您提供一个规范实现(对,迈克?!)以及足够的信息,让您能够用您选择的语言开始构建自己的实现。(尽管在开发实现时,最好始终阅读完整的规范。)
捆绑基础
首先,让我们了解 JSON Schema 草案 2020-12 中的一些关键定义。$id 关键字用于标识“schema 资源”。在下面的示例中,$id 用于标识资源,值为 https://jsonschema.dev/schemas/mixins/integer。
{
"$id": "https://jsonschema.dev/schemas/mixins/integer",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Must be an integer",
"type": "integer"
}
“复合 Schema 文档”是一个 JSON 文档,其中包含多个嵌入式 JSON Schema 资源。下面是一个简化的示例,我们稍后将对其进行分解。
{
"$id": "https://jsonschema.dev/schemas/examples/non-negative-integer-bundle",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Must be a non-negative integer",
"$comment": "A JSON Schema Compound Document. Aka a bundled schema.",
"$defs": {
"https://jsonschema.dev/schemas/mixins/integer": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://jsonschema.dev/schemas/mixins/integer",
"description": "Must be an integer",
"type": "integer"
},
"https://jsonschema.dev/schemas/mixins/non-negative": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://jsonschema.dev/schemas/mixins/non-negative",
"description": "Not allowed to be negative",
"minimum": 0
},
"nonNegativeInteger": {
"allOf": [
{
"$ref": "/schemas/mixins/integer"
},
{
"$ref": "/schemas/mixins/non-negative"
}
]
}
},
"$ref": "#/$defs/nonNegativeInteger"
}
最后,让我们根据 JSON Schema 规范仔细研究一下“捆绑”的定义。
“创建复合 Schema 文档的捆绑过程被定义为获取对外部 Schema 资源的引用(例如“$ref”),并将引用的 Schema 资源嵌入到引用文档中。捆绑应该以这样的方式完成,即基文档和任何引用/嵌入文档中使用的所有 URI(用于引用)不需要修改。”
有了这些定义,现在我们可以看看 JSON Schema 资源的定义捆绑过程!在本文中,我们只介绍理想情况。这里的目标是没有任何外部 Schema 资源。
请注意,本文不涵盖“完全反引用”,即从 schema 中删除所有 $ref 的使用。不建议这样做,而且并不总是可能的,例如当存在自引用时。
捆绑简单外部资源
在我们的第一个示例中,我们有一个捆绑的理想情况。每个模式都有一个 $id 和 $schema 定义,使捆绑过程变得简单。我们将在后面的示例中介绍各种其他情况和边缘情况,但始终建议每个资源定义自己的标识和方言。我们的主要模式资源使用就地应用器 $ref 引用了两个其他模式资源,其值为相对 URI。相对 URI 是相对于基 URI 解析的,在本例中,基 URI 在主要模式资源的 $id 值中找到。通过组合“integer”和“non-negative”模式,我们创建了一个“non-negative integer”模式。
{
"$id": "https://jsonschema.dev/schemas/mixins/integer",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Must be an integer",
"type": "integer"
}
{
"$id": "https://jsonschema.dev/schemas/mixins/non-negative",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Not allowed to be negative",
"minimum": 0
}
{
"$id": "https://jsonschema.dev/schemas/examples/non-negative-integer",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Must be a non-negative integer",
"$comment": "A JSON Schema that uses multiple external references",
"$defs": {
"nonNegativeInteger": {
"allOf": [
{
"$ref": "/schemas/mixins/integer"
},
{
"$ref": "/schemas/mixins/non-negative"
}
]
}
},
"$ref": "#/$defs/nonNegativeInteger"
}
如果“non-negative-integer”模式用作实现中的主要模式,则其他模式需要对实现可用。此时,该实现如何加载模式并不重要,因为它们在 $id 中定义了完全限定的 URI 作为其标识。任何加载模式的实现都应该构建一个内部本地索引,其中包含 $id 中定义的模式 URI 和模式资源。
请记住,任何为 $id 提供值的模式都被视为模式资源。
让我们解析(取消引用)我们主要模式中的一个引用。“$ref”: “/schemas/mixins/integer” 通过遵循首先确定基 URI,然后相对于该基 URI 解析相对 URI 的规则,解析为 https://jsonschema.dev/schemas/mixins/integer 的完全限定 URI。然后,实现应该检查其模式标识符和模式资源的内部索引,找到匹配项,并使用相应的先前加载的模式资源。
捆绑过程已完成。先前外部引用的模式按原样复制到我们主要模式中的 $defs 中。$defs 对象的键是标识 URI,但它们可以是任何东西,因为这些值不会被引用(如果您愿意,它们可以是 UUID)。看一下我们最终的捆绑模式……我的意思是“复合模式文档”,我们现在在一个模式文档中嵌入了多个模式资源。
{
"$id": "https://jsonschema.dev/schemas/examples/non-negative-integer-bundle",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Must be a non-negative integer",
"$comment": "A JSON Schema Compound Document. Aka a bundled schema.",
"$defs": {
"https://jsonschema.dev/schemas/mixins/integer": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://jsonschema.dev/schemas/mixins/integer",
"description": "Must be an integer",
"type": "integer"
},
"https://jsonschema.dev/schemas/mixins/non-negative": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://jsonschema.dev/schemas/mixins/non-negative",
"description": "Not allowed to be negative",
"minimum": 0
},
"nonNegativeInteger": {
"allOf": [
{
"$ref": "/schemas/mixins/integer"
},
{
"$ref": "/schemas/mixins/non-negative"
}
]
}
},
"$ref": "#/$defs/nonNegativeInteger"
}
当捆绑的模式最初加载和评估时,实现应该创建它自己内部的模式标识符和模式资源索引,就像以前一样。用于引用这些模式资源的相对 URI 不需要更改。
查看此捆绑模式按预期工作的最简单方法是将其粘贴到 https://json-schema.hyperjump.io 中,然后尝试实例的不同值。我希望在未来几个月内对 https://jsonschema.dev 进行一些更新,但随着我们继续将 JSON Schema 提升为一个组织,现在时间非常紧张。
值得记住的是,本文中的示例展示了理想情况,即遵循最佳实践。JSON Schema 规范确实定义了处理非理想情况和边缘情况的额外流程(例如,当 $id 或 $schema 未设置时),但是,一些解决方案可能与复合 JSON Schema 文档间接相关。例如,建立基 URI 遵循 RFC3986 中列出的步骤,JSON Schema 没有重新定义。
OpenAPI 规范示例
让我们看一个它如何与 OpenAPI 定义一起工作的示例。
openapi: 3.1.0
info:
title: API
version: 1.0.0
components:
schemas:
non-negative-integer:
$ref: 'https://jsonschema.dev/schemas/examples/non-negative-integer'
我们从输入的 OpenAPI 3.1.0 规范文档开始。为了简洁起见,我们只显示了包含单个组件的 components 部分,但假设文档的其他部分使用了组件模式“non-negative-integer”。
“non-negative-integer” 对 JSON Schema 资源有一个引用。引用 URI 是一个绝对 URI,包括域和路径,这意味着无需执行任何“将相对 URI 相对于基 URI 解析”操作。
所有解析和捆绑引用所需的模式都提供给捆绑工具。在将模式加载到实现中后,它们的原始物理位置不再重要。
openapi: 3.1.0
info:
title: API
version: 1.0.0
components:
schemas:
# This name has not changed, or been replaced, as it already existed and is likely to be referenced elsewhere
non-negative-integer:
# This Reference URI hasn't changed
$ref: 'https://jsonschema.dev/schemas/examples/non-negative-integer'
# The path name already existed. This key doesn't really matter. It could be anything. It's just for human readers. It could be an MD5!
non-negative-integer-2:
$schema: 'https://json-schema.org/draft/2020-12/schema'
$id: 'https://jsonschema.dev/schemas/examples/non-negative-integer'
description: Must be a non-negative integer
$comment: A JSON Schema that uses multiple external references
$defs:
nonNegativeInteger:
allOf:
# These references remain unchanged because they rely on the base URI of this schema resource
- $ref: /schemas/mixins/integer
- $ref: /schemas/mixins/non-negative
$ref: '#/$defs/nonNegativeInteger'
integer:
$schema: 'https://json-schema.org/draft/2020-12/schema'
$id: 'https://jsonschema.dev/schemas/mixins/integer'
description: Must be an integer
type: integer
non-negative:
$schema: 'https://json-schema.org/draft/2020-12/schema'
$id: 'https://jsonschema.dev/schemas/mixins/non-negative'
description: Not allowed to be negative
minimum: 0
这些模式被插入到 OAS 文档的 components/schemas 位置。在 schemas 对象中使用的键对引用解析没有重要性,尽管您需要避免潜在的重复。引用不需要更改,处理结果捆绑或复合文档的处理器应该在 OAS 文档中查找嵌入式模式资源的使用,跟踪 $id 值。
但是……
你们中敏锐的人可能已经注意到,复合文档可能无法使用文档根目录中定义的方言的元模式进行正确验证。我们的一位主要贡献者提炼了一个很棒的解释,他同意让我们与大家分享。
“如果嵌入式模式的 $schema 与父模式不同,那么复合模式文档将无法在没有将其分解为单独的模式资源并将适当的元模式应用于每个资源的情况下,使用元模式进行验证。这并不意味着复合模式文档在没有分解的情况下不可用,只是意味着实现需要意识到 $schema 在评估期间可能会发生变化,并相应地处理这些变化。” - Jason Desrosiers。
如果您想更深入地了解边缘情况,请告诉我们。
您可以通过 @jsonschema 或我们的 Slack 服务器 与我们联系。
我希望您同意,本在这里为我们所有人澄清了这个过程,我们可以使用这个例子来完全满足 JSON Schema 在编写将多个资源捆绑到复合 OpenAPI 文档的工具时的捆绑期望。谢谢,本! - Mike
由 vanitjan 创建的商业照片 - www.freepik.com