跳至主要内容

JSON Schema 捆绑终于正式化

作者: 2021 年 8 月 23 日博客

本文最初发表在 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。

许多工具不支持引用外部资源。捆绑是一种方便的方法,可以将跨多个文件分布的模式资源打包成一个文件,以便在其他地方使用,例如 OpenAPI 文档。

现有解决方案?新的解决方案!

有几个库提供捆绑解决方案,但它们都有局限性,到目前为止,我还没有看到任何完全支持 JSON Schema 的库。这些库中最流行的是 json-schema-ref-parser,但是它 报告 说它不打算支持 JSON Schema,而只是为了涵盖 JSON Reference 规范(现在已重新捆绑到 JSON Schema 规范中)。

我们希望为您提供一个规范实现(没错,迈克?!)以及足够的信息来帮助您开始使用您选择的语言构建自己的实现。(不过,在开发实现时,最好始终阅读完整的规范。)

捆绑基础

首先,让我们了解 JSON Schema 草案 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 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 规范精心制作的“捆绑”定义

“创建复合模式文档的捆绑过程被定义为获取对外部模式资源的引用(例如“$ref”),并将引用的模式资源嵌入到引用文档中。捆绑应该以这样的方式进行,即基文档和任何被引用/嵌入文档中的所有 URI(用于引用)不需要更改。”

有了这些定义,现在我们可以看看 JSON 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 服务器 上联系我们。

我相信你会同意的,Ben 为我们所有人阐明了流程,我们可以用这个例子在编写将多个资源捆绑到复合 OpenAPI 文档的工具时,完全满足 JSON Schema 的捆绑预期。谢谢,Ben! – Mike

商务照片由 vanitjan 创建 – www.freepik.com