2022年8月31日(水曜日) 6約 分で読めます

ジェネリック型をサポートするための動的参照の使用

最もよくある質問の1つは、静的型付けプログラミング言語の概念をJSON Schemaでどのように表現するかということです。クラス階層、ポリモーフィズム、ジェネリックなどです。これらの概念は静的型付け言語を定義し、データモデルに影響を与えます。

この記事のトピックは、定義されたデータモデルとジェネリック型のようなものをサポートするプログラミングパラダイムであれば適用できます。これは、プログラミング言語におけるデータモデリングとJSON Schemaの関係に関する、非連続的なシリーズの最初のものとなるでしょう。

今日は、ジェネリック、テンプレート、またはその他のラベルについて説明します。まず、「ジェネリック型」が何を意味するのかを説明しましょう。これはジェネリック型のレッスンを意図したものではなく、全員が同じ理解をしていることを確認したいだけです。

ジェネリック型

「ジェネリック型」とは、多くのプログラミング言語で、完全なものにするために1つ以上のセカンダリ型に関する追加情報が必要な型を作成する機能を意味します。オブジェクト指向プログラミングでは、ジェネリックはデータモデルだけでなくサービスにも適用できますが、JSON Schemaを使用してデータモデルを記述するため、このユースケースで重要なジェネリック型はラッパーとコンテナであるとほぼ確実に言えます。

.Net(C#)では、これらは型名の末尾にある山括弧で示されます。例として、List<T>Dictionary<TKey, TValue> があります。TTKey、および TValue はセカンダリ型を表します。(これらの例はどちらもコンテナ型ですが、Cloud Events(例:CloudEvent<T>)のようなエンベロープ型でも使用できます。)

C++では、これらの型は「テンプレート」と呼ばれ、キーワードtemplateと、その後の追加の型情報(山括弧内)で示されます。

1template <class T>
2class List { ... }
3
4template <class TKey, class TValue>
5class Dictionary { ... }

TypeScriptにもこの概念があり、主にC#の構文に従います。

これらの型は、必要なセカンダリ型を定義することで完成します。これは通常、型プレースホルダー(例:T)をセカンダリ型で置き換えることによって行われます。そのため、C#の例ではList<string>Dictionary<string, int>のようになります。興味深い結果の1つは、ジェネリック型はそれ自体ではインスタンス化できないことです。コンパイラやスクリプトエンジン(またはコードを実行するもの)が型に関するさまざまな情報(メモリフットプリントなど)を知ることができるように、セカンダリ型が必要です。

そこで、部分的に定義された型をJSON Schemaでどのように表現するかという問題になります。

$dynamicRef$dynamicAnchorを使用したジェネリックの表現

動的キーワード(総称して$dynamic*)は、評価時まで解決できない参照を可能にします。これは$refとは異なり、スキーマだけで静的に解決できます。通常、これはスキーマが条件式(if / then / else)も定義している場合に最も顕著に見られ、評価されているJSONインスタンスに基づいて解決が変化する可能性があります。

しかし、ジェネリックをサポートするには、この動的な動作を少し異なる方法で使用します。使用する戦略は2段階のプロセスです。

  1. ジェネリック型自体については、検証に常に失敗するサブスキーマに最初に解決される参照を持つスキーマを作成します。
  2. 各派生に対して、
    • 1.のジェネリック型スキーマを参照するサブスキーマを作成します。
    • 同じ参照に対して、セカンダリ型を記述する新しいサブスキーマを定義します。

動作を確認するために、上記のList<T>のスキーマを作成します。次に、それを利用してList<string>List<int>を定義するスキーマを2つ作成します。

ジェネリックスキーマ:List<T>

まずは単純に、もののリストから始めます。

スキーマ
{ "$schema": "https://json-schema.dokyumento.jp/draft/2020-12/schema", "$id": "https://json-schema.example/list-of-t", "type": "array"}

次に、アイテムを定義します。ここで$dynamic*が機能します。

スキーマ
{ "$schema": "https://json-schema.dokyumento.jp/draft/2020-12/schema", "$id": "https://json-schema.blog/list-of-t", "$defs": { "content": { "$dynamicAnchor": "T", "not": true } }, "type": "array", "items": { "$dynamicRef": "#T" }}

注記 ここでは、C#のList<T>と対応させるためにTを使用しました。任意の名前を使用できます。

このスキーマのみを使用してインスタンスを検証する場合、"$dynamicRef": "#T"は、/$defs/contentに含まれる"$dynamicAnchor": "T"を持つサブスキーマに解決されます。この場合、$dynamicRef$dynamicAnchorは、$ref$anchorと同様に機能します。

"not": trueは、すべてのインスタンスが検証に失敗することを意味します。通常、すべてのインスタンスが検証に失敗することを確認するには、falseスキーマを使用しますが、この場合は動的アンカーを含める必要があるため、単純なfalseは機能しません。"not": trueが最も簡潔な代替案だと思いますが、"allOf": [ false ]なども使用できます。

注記 空の配列は依然としてこのスキーマの検証に合格しますが、アイテムを含む配列はすべて失敗します。

複数の動的アンカーを使用して、複数のセカンダリ型を必要とするDictionary<TKey, TValue>のような型もサポートできます。

スキーマ
{ "$schema": "https://json-schema.dokyumento.jp/draft/2020-12/schema", "$id": "https://json-schema.blog/list-of-t", "$defs": { "key": { "$dynamicAnchor": "TKey", "not": true }, "value": { "$dynamicAnchor": "TValue", "not": true } }, "type": "array", "items": { "type": "object", "properties": { "key": { "$dynamicRef": "#TKey" }, "value": { "$dynamicRef": "#TValue" } } }}

ジェネリック型については以上です。魔法はコンテンツを定義したときに現れます。

コンテンツの定義

前述のように、list-of-tを参照し、Tの定義も提供するスキーマが必要です。List<string>のスキーマを作成してみましょう。

スキーマ
{ "$schema": "https://json-schema.dokyumento.jp/draft/2020-12/schema", "$id": "https://json-schema.blog/list-of-string", "$defs": { "string-items": { "$dynamicAnchor": "T", "type": "string" } }, "$ref": "list-of-t"}

動作の説明

  1. 評価が始まると、ルートスキーマ(list-of-string)から始まる「動的スコープ」が作成され、評価全体を通して維持されます。
  2. このルートスキーマは、"$dynamicAnchor": "T" を定義しています。
  3. 次に、評価は$refを使用してジェネリックスキーマlist-of-tを参照します。これは新しい *字句* スコープですが、*動的* スコープは変更されません。
  4. ジェネリックスキーマも"$dynamicAnchor": "T"を宣言していますが、その動的アンカーは既に定義されているため、新しい宣言は無視されます。
  5. 評価が"$dynamicRef": "#T"に到達すると、動的スコープの先頭にある最初のものが使用されます。

stringではなくint項目が必要な場合は、$dynamicAnchorを持つサブスキーマが整数を定義する新しいスキーマを作成するだけです。

スキーマ
{ "$schema": "https://json-schema.dokyumento.jp/draft/2020-12/schema", "$id": "https://json-schema.blog/list-of-int", "$defs": { "int-items": { "$dynamicAnchor": "T", "type": "integer" } }, "$ref": "list-of-t"}

結論

$dynamicRef$dynamicAnchor を使用することで、構造は同じだがコンテンツタイプが異なるクラスのスキーマを完全に記述する必要がなくなります。代わりに、構造に関する部分的で再利用可能なスキーマを作成できます。これにより、完全に定義されたスキーマのサイズが大幅に縮小され、保守が容易になります。

補足 Draft 2020-12では、$dynamicRef が機能するためには、汎用スキーマに $dynamicAnchor を含める必要があります。将来のバージョンでは、これは厳密には必要ないため、この要件は削除されます。解決の試みは単純に失敗します。(この要件は、Draft 2019-09の前身である $recursive*からの名残です。) しかし、汎用型のモデリングという特定の用途では、List<T> のような汎用型をインスタンス化できないことに対するアナロジーとして機能するため、引き続き含めることをお勧めします。最終的な結果は同じ(検証失敗)ですが、明示的に含めることで意図をより明確に記述できると考えます。

表紙写真は、ニック・フィウィングス氏によるUnsplashからの写真(リンク)に、私がいくつかの編集を加えたものです。 😁