本文最初发布在 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 是 JSON Schema 的版本,OAS 在 3.1.0 版本之前使用;或者更确切地说,它是 JSON Schema 的子集/超集。)
事实上,捆绑,如果有什么不同的话,将比以往任何时候都更重要。OAS 3.1 引入对全面 JSON Schema 的支持,极大地增加了使用现有 JSON Schema 文档的开发人员使用它们作为参考在新的和更新的 OpenAPI 定义中使用它们可能性。最终的真实来源很重要,通常是 JSON Schema。
许多工具不支持引用外部资源。捆绑是一种便捷的方式,可以将分散在多个文件中的架构资源打包到一个文件中,以便在其他地方使用,例如 OpenAPI 文档。
现有的解决方案?新的解决方案!
有几个库提供捆绑解决方案,但它们都有局限性,到目前为止,我还没有看到任何完全支持 JSON 架构的库。其中最受欢迎的库叫做 json-schema-ref-parser,但是它 报告 说它并非旨在支持 JSON 架构,而只是为了覆盖 JSON 引用规范(现在已被捆绑回 JSON 架构规范)。
我们希望为您提供一个规范的实现(对吧,迈克?!)以及足够的信息,让您能够用您选择的语言开始构建自己的实现。(虽然,在开发实现时始终最好阅读完整规范。)
捆绑基础
首先,让我们看看 JSON 架构草案 2020-12 中的一些关键定义。 $id 关键字用于标识“架构资源”。在下面的示例中,$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"
}
“复合架构文档”是一个 JSON 文档,它包含多个嵌入式 JSON 架构资源。下面是一个简化的示例,我们将在稍后进行解析。
{
"$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 架构规范精心制作的“捆绑”定义。
“用于创建复合架构文档的捆绑过程被定义为将对外部架构资源的引用(例如“$ref”)嵌入到引用文档中。捆绑 SHOULD 以一种方式完成,即基本文档中以及任何被引用/嵌入的文档中使用的所有 URI(用于引用)不需要更改。”
有了这些定义,我们现在就可以看看为 JSON 架构资源定义的捆绑过程!我们只会在本文中介绍理想情况。这里的目标是没有任何外部架构资源。
请注意,本文不涵盖“完全反引用”,即从架构中删除所有 $ref 的用法。不建议这样做,而且并非总是可行的,例如,当存在自引用时。
捆绑简单外部资源
在我们的第一个示例中,我们有一个理想的捆绑情况。每个架构都定义了 $id 和 $schema,使得捆绑过程变得很简单。我们将涵盖各种其他情况和边缘案例,但在后续示例中,让每个资源定义自己的标识和方言始终是可取的。我们的主要架构资源使用 $ref 原地应用器引用了其他两个架构资源,其值为相对 URI。相对 URI 相对于基 URI 解析,在本文中,基 URI 在主要架构资源的 $id 值中找到。通过组合“整数”和“非负数”架构,我们创建了一个“非负整数”架构。
{
"$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"
}
如果“非负整数”架构在实现中用作主要架构,则其他架构需要对实现可用。在这一点上,实现加载架构的方式并不重要,因为它们在 $id 中定义了完全限定的 URI 作为其标识。任何加载架构的实现都应该构建一个架构 URI 在 $id 中定义的架构资源的内部本地索引。
请记住,任何为 $id 提供值的架构都被视为架构资源。
让我们解析(反引用)主要架构中的一个引用。 “$ref”: “/schemas/mixins/integer” 通过遵循首先确定基 URI,然后将相对 URI 相对于该基 URI 解析的规则,解析为完全限定的 URI https://jsonschema.dev/schemas/mixins/integer 。然后,实现应该检查其架构标识符和架构资源的内部索引,找到匹配项,并使用相应的先前加载的架构资源。
捆绑过程已完成。先前被外部引用的架构被原封不动地复制到主要架构中的 $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 架构提升为一个组织。
值得记住的是,本文中的示例展示了理想情况,即在遵循最佳实践的情况下。JSON 架构规范确实定义了针对非理想情况和边缘案例的其他过程(例如,当 $id 或 $schema 未设置时),但是,某些解决方案可能与复合 JSON 架构文档间接相关。例如,建立基 URI 遵循 RFC3986 中规定的步骤,JSON 架构没有重新定义这些步骤。
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 规范文档开始。为了简洁起见,我们只展示了包含单个组件的组件部分,但假设文档的其他部分使用了组件架构“非负整数”。
“非负整数”对 JSON 架构资源只有一个引用。引用 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 在评估期间可能会发生变化,并适当地处理这些变化。” – 杰森·德罗西尔斯。
如果您想更深入地了解边缘案例,请告诉我们。
您可以通过 @jsonschema 或我们的 Slack 服务器联系我们。
我希望您同意,本在这里为我们大家澄清了这个过程,我们可以使用这个例子来完全满足 JSON 架构在编写将多个资源捆绑到复合 OpenAPI 文档中的工具时的捆绑预期。谢谢,本! – 迈克
由 vanitjan 创建的商业照片 – www.freepik.com