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 中的 Schema 假定使用 OpenAPI 3.1 的自訂 JSON Schema 方言。此方言完整支援所有 draft 2020-12 的功能,外加一些額外的關鍵字和 format 值。

使用預設方言驗證

有兩種 Schema 可用於驗證 OpenAPI 3.1 文件。https://spec.openapis.org/oas/3.1/schema 包含驗證文件的所有約束,除了 Schema 之外。您不應該單獨根據此 Schema 驗證您的 OpenAPI 文件。將此 Schema 視為一個抽象 Schema,旨在擴充以包含您正在使用的 JSON Schema 方言的 Schema 驗證支援。

這就是為什麼還有 https://spec.openapis.org/oas/3.1/schema-base,它使用 OpenAPI 3.1 Schema 方言的驗證支援來擴充抽象 Schema。如果您使用的是純粹開箱即用的 OpenAPI 3.1,這是您要用來驗證文件的 Schema。如果您想使用不同的方言,請繼續閱讀以了解如何擴充主要 Schema,以取得您選擇的方言的驗證支援。

這是透過 JSON Schema 2020-12 中新增的動態參考來實現的。動態參考如何運作的詳細資訊不在本文的範圍內,但我們將涵蓋足夠的內容,讓您可以為您在 OpenAPI 3.1 文件中選擇使用的任何方言建立自己的具體 Schema。

範例

這些範例使用 @hyperjump/json-schema 來驗證 OpenAPI 文件。請注意,動態參考是 JSON Schema 的相對較新功能,許多驗證器尚未支援它們,或僅有有限的支援,或存在錯誤。

不含 Schema 驗證

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

使用 OpenAPI Schema 方言 Schema 驗證

1import { validate } from "@hyperjump/json-schema/openapi-3-1";
2
3(async function () {
4  const validateOpenApi = await validate("https://spec.openapis.org/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 中的一些選取內容。

這是定義 Schema 物件的位置。$dynamicAnchor 宣告此子 Schema 為可由另一個 Schema 有效覆寫的內容。如果未覆寫,則預設行為是驗證值是否為物件或布林值。不會對 Schema 執行其他驗證。

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

當此 Schema 中的某些內容想要參考 Schema 物件時,它不會像平常一樣參考 #/$defs/schema,而是使用動態參考來參考先前選取中設定的「meta」動態錨點。現在,另一個 Schema 可以覆寫它解析到的位置,而不是總是解析為 #/$defs/schema

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

根據預設方言驗證 Schema 物件

有了這些模糊的基礎構建模組,讓我們導出一個 Schema,該 Schema「擴充」抽象 Schema,以建立一個使用預設方言 meta-schema 驗證 Schema 物件的 Schema。

第一步是包含抽象 Schema。

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

然後,我們需要新增一個 $dynamicAnchor,它與抽象 Schema 中的 $dynamicAnchor 相符,以覆寫對「meta」的動態參考將解析到的位置。從那裡,我們可以參考預設方言的 meta Schema。

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

這足以取得我們想要的 Schema 物件驗證,但還有一些鬆散的結尾也需要處理。jsonSchemaDialect OpenAPI 3.1 文件中的欄位可用於變更所使用的方言。由於此 Schema 僅支援預設方言,我們希望限制人們將其變更為其他方言。如果他們需要變更它,他們需要一個不同的 Schema 來進行驗證。我們也不希望人們使用 $schema 關鍵字來變更個別 Schema 的方言。

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

有了這個,我們就擁有您在官方 https://spec.openapis.org/oas/3.1/schema-base Schema 中找到的內容。

支援多種方言

隨著 JSON Schema 2020-12 的採用,支援了 $id$schema 關鍵字,它們共同讓我們可以覆寫 Schema 的預設 JSON Schema 方言。假設我們有一個 OpenAPI 3.1 文件,預設情況下我們使用 JSON Schema 2020-12,但我們也有一些想要使用的舊 JSON Schema draft-07 Schema。

1jsonSchemaDialect: 'https://json-schema.dev.org.tw/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.dev.org.tw/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,預設情況下,Schema 不會理解 OpenAPI 3.1 詞彙中新增的關鍵字,例如 discriminator。只會識別標準 JSON Schema 2020-12 關鍵字。

/components/schemas/foo Schema 被理解為以 JSON Schema 2020-12 解釋,因為這是我們設定的預設值。

/components/schemas/bar Schema 將該 Schema 的方言變更為 JSON Schema draft-07。有幾個因素共同作用使其成為可能。$schema 關鍵字設定 Schema 的方言,但 $schema 僅允許出現在它出現的文件的根目錄中。這就是為什麼我們還需要包含 $id 關鍵字的原因。$id 關鍵字有效地將該 Schema 設定為具有其自己的識別碼的獨立文件,且該位置為根目錄。它是嵌入在 OpenAPI 3.1 文件中的獨立文件。您可以將其視為 HTML 中的 iframe。

這樣做的結果是 /components/schemas/bar 無法使用本機參考,例如 #/components/schemas/foo,來參考 /components/schemas 中的另一個 Schema,因為它現在在技術上位於不同的文件中。有兩種方法可以解決這個問題。一個選項是使用對 OpenAPI 3.1 文件的外部參考,例如 myapi.openapi.yml#/components/schemas/foo。另一個選項是也為 /components/schemas/foo 提供 $id,並改為參考該 ./schemas/foo

驗證

現在我們了解它是如何運作的,讓我們導出一個 Schema,以驗證預設方言為 JSON Schema 2020-12 且允許 JSON Schema draft-07 作為替代方案的 OpenAPI 3.1 文件。

1$schema: 'https://json-schema.dev.org.tw/draft/2020-12/schema'
2
3$ref: 'https://spec.openapis.org/oas/3.1/schema'
4properties:
5  jsonSchemaDialect:
6    const: 'https://json-schema.dev.org.tw/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.dev.org.tw/draft/2020-12/schema'
17          - 'https://json-schema.dev.org.tw/draft-07/schema#'
18    allOf:
19      - if:
20          properties:
21            $schema:
22              const: 'https://json-schema.dev.org.tw/draft/2020-12/schema'
23        then:
24          $ref: 'https://json-schema.dev.org.tw/draft/2020-12/schema'
25      - if:
26          type: object
27          properties:
28            $schema:
29              const: 'https://json-schema.dev.org.tw/draft-07/schema#'
30          required:
31            - $id
32            - $schema
33        then:
34          $ref: 'https://json-schema.dev.org.tw/draft-07/schema'

第一個變更是現在需要 jsonSchemaDialect 欄位,因為我們不再使用預設值。

接下來,我們必須更新 Schema 定義,以僅允許我們想要允許的方言的 $schema 值。

如果沒有使用 $schema 關鍵字,或是 $schema 設定為 JSON Schema 2020-12,則第一個 if/then 將會驗證 schema 為 JSON Schema 2020-12 的 schema。當然,在這種情況下不需要使用 $schema,但允許使用。

如果存在 $id$schema 指示為 draft-07,則第二個 if/then 將會驗證 schema 為 JSON Schema draft-07 的 schema。

您可以擴展此模式來支援您想要支援的任意數量的方言。

圖片來源:Gonzalo FacelloUnsplash