2021年12月8日(水曜日) 8約○分

OpenAPIとJSON Schemaの検証

OpenAPI 3.1のリリース以降、OpenAPIドキュメントで使用されるJSON Schemaの方言は設定可能になりました。デフォルトではOpenAPI 3.1 Schemaの方言が使用されますが、draft 2020-12または任意の他の方言を選択することもできます。これは、そのコンポーネント(JSON Schema)がオープンエンドである場合に、OpenAPI 3.1ドキュメントをどのように検証するかという問題を引き起こします。この記事では、OpenAPI 3.1ドキュメントのデフォルトのJSON Schemaの方言を設定する方法、および選択した方言に関係なく、JSON Schemaを含むドキュメントを検証する方法について説明します。

JSON Schemaの方言とは?

このコンテキストにおける「方言」という用語に慣れていない人もいるため、先に定義しておきましょう。JSON Schemaの方言とは、JSON Schemaの任意の一意の具体化です。これには、draft-07やdraft 2020-12などのJSON Schemaの公式リリースが含まれますが、JSON Schemaのカスタムバージョンも含まれます。OpenAPIでは、2.0、3.0、3.1で効果的に3つの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ドキュメントを検証するためのスキーマは2つあります。https://spec.openapis.org/oas/3.1/schemaには、スキーマを除くドキュメントの検証に関するすべての制約が含まれています。OpenAPIドキュメントをこのスキーマ単体で検証することは想定されていません。このスキーマは、使用しているJSON Schemaの方言に対するスキーマ検証のサポートを含めることを目的とした、抽象的なスキーマと考えてください。

そのため、https://spec.openapis.org/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/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/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スキーマの一部を見てみましょう。

ここでスキーマオブジェクトが定義されています。$dynamicAnchorは、このサブスキーマを、別のスキーマによって効果的にオーバーライドできるものとして宣言します。オーバーライドされない場合、デフォルトの動作は、値がオブジェクトまたはブール値であることを検証することです。スキーマに対してはそれ以外の検証は実行されません。

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

このスキーマ内の何かがスキーマオブジェクトを参照する場合、通常の#/$defs/schemaを参照する代わりに、前のセレクションに設定された「meta」動的アンカーへの動的参照を使用します。これにより、常に#/$defs/schemaに解決されるのではなく、別のスキーマが解決先をオーバーライドする可能性があります。

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

デフォルトの方言に対するスキーマオブジェクトの検証

これらの曖昧な構成要素を念頭に置いて、抽象スキーマを「拡張」して、デフォルトの方言メタスキーマを使用してスキーマオブジェクトを検証するスキーマを導き出してみましょう。

最初のステップは、抽象スキーマを含めることです。

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

次に、「meta」への動的参照の解決先をオーバーライドするために、抽象スキーマ内のものと一致する$dynamicAnchorを追加する必要があります。そこから、デフォルトの方言のメタスキーマを参照できます。

1$schema: 'https://json-schema.dokyumento.jp/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'

これだけで、目的のスキーマオブジェクト検証が得られますが、いくつかの未解決の課題も解決する必要があります。jsonSchemaDialectフィールドは、OpenAPI 3.1ドキュメントで使用される方言を変更するために使用できます。このスキーマはデフォルトの方言のみをサポートするため、ユーザーがそれを他のものに変更することを制限する必要があります。変更する必要がある場合は、検証する別のスキーマが必要です。$schemaキーワードを使用して個々のスキーマの方言を変更することも望ましくありません。

1$schema: 'https://json-schema.dokyumento.jp/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スキーマにあるものとまったく同じものができました。

複数の方言のサポート

JSON Schema 2020-12の採用により、$id$schemaキーワードがサポートされるようになりました。これらを組み合わせることで、スキーマのデフォルトのJSON Schemaの方言をオーバーライドできます。デフォルトでJSON Schema 2020-12を使用するOpenAPI 3.1ドキュメントがあり、レガシーなJSON Schema draft-07スキーマも使用したいと仮定しましょう。

1jsonSchemaDialect: 'https://json-schema.dokyumento.jp/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.dokyumento.jp/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に設定することで、デフォルトでは、スキーマはdiscriminatorなどのOpenAPI 3.1ボキャブラリに追加されたキーワードを理解しません。標準の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 内の別のスキーマを参照することができません。これは技術的に異なるドキュメント内にあるためです。これを回避するには2つの方法があります。1つの方法は、myapi.openapi.yml#/components/schemas/foo のようなOpenAPI 3.1ドキュメントへの外部参照を使用することです。もう1つの方法は、/components/schemas/foo にも $id を付与し、代わりに ./schemas/foo を参照することです。

検証

この仕組みを理解したところで、JSON Schema 2020-12をデフォルトのダイアレクト、JSON Schema draft-07を代替案として許可するOpenAPI 3.1ドキュメントを検証するためのスキーマを導出しましょう。

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

最初の変更点は、デフォルトを使用しなくなったため、jsonSchemaDialect フィールドが必須になったことです。

次に、許可するダイアレクトの$schema値のみを許可するようにスキーマ定義を更新する必要があります。

最初のif/then は、$schemaキーワードが使用されていない場合、または$schemaがJSON Schema 2020-12に設定されている場合、スキーマをJSON Schema 2020-12スキーマとして検証します。この場合、$schemaを使用する必要はありませんが、許可されています。

2番目のif/then は、$idとdraft-07を示す$schemaがある場合、スキーマをJSON Schema draft-07スキーマとして検証します。

このパターンは、サポートするダイアレクトの数だけ拡張できます。

写真提供: Gonzalo Facello on Unsplash