2021 年 12 月 8 日,星期三 ·8分钟阅读

验证 OpenAPI 和 JSON Schema

从 OpenAPI 3.1 发布开始,OpenAPI 文档中使用的 JSON Schema 方言就可配置了。默认情况下,您将获得 OpenAPI 3.1 Schema 方言,但如果需要,您可以选择使用 draft 2020-12 或其他任何方言。这就提出了一个问题:如果 OpenAPI 3.1 文档的某个组件(JSON Schema)是开放的,该如何验证该文档呢?在本文中,我们将介绍如何在 OpenAPI 3.1 文档中配置默认 JSON Schema 方言,以及如何验证该文档(包括 JSON Schema),无论您选择使用哪些方言。

什么是 JSON Schema 方言?

因为并非所有人都熟悉这种情况下“方言”一词,所以让我们花点时间在继续之前对其进行定义。JSON Schema 方言是 JSON Schema 的任何独特化身。这包括 JSON Schema 的任何官方版本,如 draft-07 或 draft 2020-12,但也包括 JSON Schema 的自定义版本。OpenAPI 在 2.0、3.0 和 3.1 中引入了三种 JSON Schema 方言。JSON Schema 方言与 JSON Schema 的核心架构兼容,但可能会添加关键字、删除关键字或修改关键字的行为。

OpenAPI 3.1 Schema 方言

默认情况下,OpenAPI 3.1 中的模式被假定为使用 OpenAPI 3.1 的自定义 JSON Schema 方言。该方言包括对所有 draft 2020-12 功能的完全支持,以及一些额外的关键字和format值。

使用默认方言进行验证

有两种模式可用于验证 OpenAPI 3.1 文档。https://spec.openapis.org.cn/oas/3.1/schema包括验证文档的所有约束,除了模式之外。您不应单独针对此模式验证 OpenAPI 文档。将此模式视为一个抽象模式,它旨在扩展以包括对您正在使用的 JSON Schema 方言的模式验证支持。

这就是为什么还存在https://spec.openapis.org.cn/oas/3.1/schema-base,它将抽象模式扩展为对 OpenAPI 3.1 Schema 方言的验证支持。如果您使用的是开箱即用的 OpenAPI 3.1,那么这就是您要针对其验证文档的模式。如果您想使用不同的方言,请继续阅读以了解如何扩展主模式,为您的选择的方言获得验证支持。

这得益于在 JSON Schema 2020-12 中添加的动态引用。本文档将不介绍动态引用的工作原理,但我们将涵盖足够的知识,以便您为 OpenAPI 3.1 文档中选择的任何方言创建自己的具体模式。

示例

这些示例使用@hyperjump/json-schema来验证 OpenAPI 文档。请注意,动态引用是 JSON Schema 的一个相对较新的功能,许多验证器尚未支持它们,或者支持有限,或者存在错误。

无模式验证

1import { validate } from "@hyperjump/json-schema/openapi-3-1";
2
3const validateOpenApi = await validate("https://spec.openapis.org.cn/oas/3.1/schema");
4
5const example = YAML.parse(await readFile("./example.openapi.json"));
6const result = validateOpenApi(example);
7console.log(result);

使用 OpenAPI Schema 方言模式验证

1import { validate } from "@hyperjump/json-schema/openapi-3-1";
2
3(async function () {
4  const validateOpenApi = await validate("https://spec.openapis.org.cn/oas/3.1/schema-base");
5
6  const example = YAML.parse(await readFile("./example.openapi.json"));
7  const result = validateOpenApi(example);
8  console.log(result);
9}());

它是如何工作的?

为了了解其工作原理,让我们看看 OpenAPI 3.1 模式中的几个选择。

这是定义 Schema 对象的位置。$dynamicAnchor声明此子模式是可以被其他模式有效覆盖的内容。如果未覆盖,则默认行为是验证该值为对象或布尔值。不会对模式进行其他验证。

1$defs:
2  schema:
3    $dynamicAnchor: meta
4    type:
5      - object
6      - boolean

当此模式中的某些内容想要引用 Schema 对象时,它不会像往常一样引用#/$defs/schema,而是使用动态引用到之前选择中设置的“meta”动态锚点。现在,它不再总是解析为#/$defs/schema,而是其他模式可能会覆盖它解析到的位置。

1$defs:
2  components:
3    type: object
4    properties:
5      schemas:
6        type: object
7        additionalProperties:
8          $dynamicRef: '#meta'

根据默认方言验证 Schema 对象

有了这些模糊的构建块,让我们推导出一个模式,该模式“扩展”抽象模式以创建使用默认方言元模式验证 Schema 对象的模式。

第一步是包含抽象模式。

1$schema: 'https://json-schema.fullstack.org.cn/draft/2020-12/schema'
2
3$ref: 'https://spec.openapis.org.cn/oas/3.1/schema/latest'

然后,我们需要添加一个$dynamicAnchor,它与抽象模式中的那个匹配,以覆盖“meta”的动态引用解析到的位置。从那里,我们可以引用默认方言的元模式。

1$schema: 'https://json-schema.fullstack.org.cn/draft/2020-12/schema'
2
3$ref: 'https://spec.openapis.org.cn/oas/3.1/schema/latest'
4
5$defs:
6  schema:
7    $dynamicAnchor: meta
8    $ref: 'https://spec.openapis.org.cn/oas/3.1/dialect/base'

这足以获得我们需要的 Schema 对象验证,但我们还需要解决几个松散的细节。jsonSchemaDialect字段可在 OpenAPI 3.1 文档中用于更改使用的方言。由于此模式仅支持默认方言,因此我们要限制人们将其更改为其他内容。如果他们需要更改它,则需要使用不同的模式进行验证。我们也不希望人们使用$schema关键字来更改单个模式的方言。

1$schema: 'https://json-schema.fullstack.org.cn/draft/2020-12/schema'
2
3$ref: 'https://spec.openapis.org.cn/oas/3.1/schema'
4properties:
5  jsonSchemaDialect:
6    $ref: '#/$defs/dialect'
7
8$defs:
9  dialect:
10    const: 'https://spec.openapis.org.cn/oas/3.1/dialect/base'
11  schema:
12    $dynamicAnchor: meta
13    $ref: 'https://spec.openapis.org.cn/oas/3.1/dialect/base'
14    properties:
15      $schema:
16        $ref: '#/$defs/dialect'

有了这些,我们就有了您在官方https://spec.openapis.org.cn/oas/3.1/schema-base模式中找到的完全内容。

支持多种方言

随着 JSON Schema 2020-12 的采用,对$id$schema关键字的支持也随之而来,它们共同允许我们覆盖模式的默认 JSON Schema 方言。让我们假设我们有一个 OpenAPI 3.1 文档,其中默认使用 JSON Schema 2020-12,但我们也有一些我们想要使用的遗留 JSON Schema draft-07 模式。

1jsonSchemaDialect: 'https://json-schema.fullstack.org.cn/draft/2020-12/schema'
2components:
3  schemas:
4    foo:
5      type: object
6      properties:
7        foo:
8          $ref: '#/components/schemas/baz'
9      unevaluatedProperties: false
10    bar:
11      $id: './schemas/bar'
12      $schema: 'https://json-schema.fullstack.org.cn/draft-07/schema#'
13      type: object
14      properties:
15        bar:
16          $ref: '#/definitions/number'
17      definitions:
18        number:
19          type: number
20    baz:
21      type: string

这里发生了什么

首先,我们使用jsonSchemaDialect字段来设置文档的默认方言。通过将默认方言设置为 JSON Schema 2020-12,默认情况下,模式将不会理解在 OpenAPI 3.1 词汇表中添加的关键字,例如discriminator。只有标准 JSON Schema 2020-12 关键字会被识别。

/components/schemas/foo模式被理解为被解释为 JSON Schema 2020-12,因为我们将其设置为默认值。

/components/schemas/bar模式将该模式的方言更改为 JSON Schema draft-07。有一些因素共同使这成为可能。$schema关键字设置模式的方言,但$schema仅允许出现在其出现的文档的根目录中。这就是为什么我们还需要包含$id关键字。该$id关键字实际上使该模式成为一个独立的文档,具有自己的标识符,以及该位置作为根目录。它是嵌入在 OpenAPI 3.1 文档中的一个独立文档。您可以将其视为 HTML 中的 iframe。

这样做的一个后果是,/components/schemas/bar不能使用本地引用,例如#/components/schemas/foo来引用/components/schemas中的另一个模式,因为它现在技术上位于另一个文档中。有两种方法可以解决这个问题。一种选择是使用对 OpenAPI 3.1 文档的外部引用,例如myapi.openapi.yml#/components/schemas/foo。另一种选择是让/components/schemas/foo也拥有一个$id,并引用该$id./schemas/foo

验证

现在我们已经了解了其工作原理,让我们推导出一个模式来验证一个 OpenAPI 3.1 文档,其中 JSON Schema 2020-12 作为默认方言,JSON Schema draft-07 作为允许的备选方案。

1$schema: 'https://json-schema.fullstack.org.cn/draft/2020-12/schema'
2
3$ref: 'https://spec.openapis.org.cn/oas/3.1/schema'
4properties:
5  jsonSchemaDialect:
6    const: 'https://json-schema.fullstack.org.cn/draft/2020-12/schema'
7required:
8  - jsonSchemaDialect
9
10$defs:
11  schema:
12    $dynamicAnchor: meta
13    properties:
14      $schema:
15        enum:
16          - 'https://json-schema.fullstack.org.cn/draft/2020-12/schema'
17          - 'https://json-schema.fullstack.org.cn/draft-07/schema#'
18    allOf:
19      - if:
20          properties:
21            $schema:
22              const: 'https://json-schema.fullstack.org.cn/draft/2020-12/schema'
23        then:
24          $ref: 'https://json-schema.fullstack.org.cn/draft/2020-12/schema'
25      - if:
26          type: object
27          properties:
28            $schema:
29              const: 'https://json-schema.fullstack.org.cn/draft-07/schema#'
30          required:
31            - $id
32            - $schema
33        then:
34          $ref: 'https://json-schema.fullstack.org.cn/draft-07/schema'

第一个变化是jsonSchemaDialect字段现在是必需的,因为我们不再使用默认值。

接下来,我们必须更新模式定义,以仅允许我们想要允许的方言的$schema值。

第一个if/then将在没有使用$schema关键字的情况下,或$schema设置为JSON Schema 2020-12时,将模式验证为JSON Schema 2020-12模式。当然,在这种情况下,使用$schema是不必要的,但它是允许的。

第二个if/then将在存在$id$schema指示草案-07的情况下,将模式验证为JSON Schema 草案-07模式。

您可以将此模式扩展到您要支持的任何数量的方言。

图片来自Gonzalo FacelloUnsplash