JSON Schema を用いた継承のモデリング
おそらく最も多く寄せられる質問は、「JSON Schema で継承階層をどのようにモデル化しますか?」です。そして、その質問に対する最も一般的な答えは「しません」です。
JSON Schema は、設計上そのようなものではありません。制約が増えるほど一致するものが少なくなる減算的なシステムであり、データモデリングは定義が増えるほど一致するものが多くなる加算的な傾向があります。これらのシステムは本質的に互換性がありません。
しかし、いくつかの妥協を受け入れるならば、何とか解決策を見つけることができるかもしれません。
私たちのモデル
始めとして、いくつかのコンピュータ周辺機器をモデル化しようとします。厳密に型付けされた言語では、すべての周辺機器に共通する多くのプロパティ(そして通常は関数)を定義するPeripheral
基本クラスを使用してこれをモデル化する場合があります。その後、各デバイスはこの基本クラスのサブクラスになります。
ここでは、基本クラスのプロパティname
のみを定義します。つまり、すべての周辺機器には名前が必要です。
コードサンプルには TypeScript を使用しますが、これらの概念は他の言語にも適用できます。
1abstract class Peripheral {
2 name: string;
3 // ...
4}
これで、この基本クラスを継承することで、他の周辺機器であるMouse
とKeyboard
を定義できます。
1class Mouse extends Peripheral {
2 buttonCount: number;
3 wheelCount: number;
4 trackingType: "ball" | "optical";
5 // ...
6}
7
8class Keyboard extends Peripheral {
9 keyCount: number;
10 mediaButtons: boolean;
11 // ...
12}
これで始めるには十分です。
制約を用いたモデル表現
JSON Schema では、理想的には、これらそれぞれにスキーマが必要です。周辺機器には、次のようなスキーマを試すことができます。
スキーマ識別子にはschema:
URI を使用しています。これは、これらのスキーマはどこにもアクセスできないためです。これは、JSON Schema の次のバージョンで検討している推奨事項です。このアプローチについてご意見をお聞かせください。
しかし、additionalProperties
キーワードは問題を引き起こします。「継承された」スキーマ(Mouse
のために構築しようとしているもののような)は追加のプロパティを定義できませんが、これは間違いなく必要です。これは全く機能せず、解決策はそれを単に省略することです。
しかし、これでname
プロパティを持つ任意のJSONオブジェクトが周辺機器として検証されます。完全に正しいとは言えませんが、これで我慢できます。これが最初の譲歩になります。
基底クラスをモデル化するスキーマは、インスタンスがそのクラスの派生を表していることを検証できません。
派生をモデル化するのは非常に簡単です。派生が定義するものをモデル化し、基底スキーマに$ref
を追加します。
{ "$schema": "https://json-schema.dokyumento.jp/draft/2020-12/schema", "$id": "schema:keyboard", "$ref": "schema:peripheral", "properties": { "keys": { "type": "integer" }, "mediaButtons": { "type": "boolean" } }, "required": [ "keys", "mediaButtons" ], "unevaluatedProperties": false}
派生スキーマの場合、これらのスキーマから派生するスキーマがないため、unevaluatedProperties
を使用できます。継承階層が大きく、これらのクラスが他のクラスの基底として機能する場合は、schema:peripheral
で行ったように、unevaluatedProperties
を省略する必要があります。追加のプロパティのチェックは、継承ツリーの葉に対してのみ実行できます。
さらに、$ref
の「内部を見る」ことができる必要があるため、additionalProperties
の代わりにunevaluatedProperties
を使用します。これにより、name
が基底スキーマの一部として評価されたことを識別できます。additionalProperties
を使用すると、name
は拒否されます。
これは非常に簡単そうで、単一の(そしてかなり簡単な)譲歩をするだけで済みました。
再帰参照の追加
周辺機器の1つに他の周辺機器が接続されている場合はどうでしょうか?例えば、USBハブなど。
1class UsbHub extends Peripheral {
2 connectedDevices: Peripheral[];
3 // ...
4}
スキーマでそれをモデル化してみましょう。
これは機能しますが、最初に譲歩したことを覚えていますか?このスキーマでは、文字列name
プロパティを持つ任意のアイテムが許可されます。しかし、これはTypeScriptモデルとは一致しません。TypeScriptモデルでは、connectedDevices
は、Peripheral
から派生した型のみを保持できるとされています。
これは一部の人にとっては十分かもしれませんが、私の意見では機能しません。connectedDevices
配列のアイテムが既知の周辺機器の種類のみであることを確認したいと考えています。そのためには、別のスキーマが必要です。
既知の派生型のみのサポート
問題:既知のデバイスの種類のいずれかを表すJSONを識別するスキーマが必要です。
解決策:既知のすべてのデバイスの種類のスキーマを参照するoneOf
を使用してスキーマを定義します。
このスキーマは非常に基本的なものです。「JSONがこれらのデバイスのいずれかに一致する場合、既知の周辺機器である」ということを示しています。
これをschema:usbhub
で参照できるようになりました。
これで、USBハブとそれに接続されたデバイスを適切に検証できるようになりました。
問題は、oneOf
にアイテムを動的に追加できないため、開発時に認識しているデバイスのみをサポートできることです。ほとんどの場合、これは問題になりません。ただし、これをパッケージとして公開して他の人が使用できるようにする予定がある場合、彼らが作成したデバイスはサポートされません。(このための回避策はありますが、良い方法ではないため、ここでは共有しません。)これが2つ目の譲歩です。
基底クラスへの参照が必要な場合、事前に認識している派生型のみをサポートできます。
予期せぬ利点
JSONがMouse
、Keyboard
、またはUsbHub
のいずれであるかを判断するために、これら3つのスキーマすべてを保持し、それぞれを順番に検証して、どれを受信したかを判断することができます。しかし、参照の問題に対する私たちの解決策は、実際により良い選択肢を与えてくれます。
私たちはschema:known-peripherals
が、(そう設計されているため)既知の周辺機器をすべて検証できることを知っていますが、より冗長な出力形式を使用すると、どのような種類の周辺機器を受け取ったかを実際に教えてくれます。
まず、その子出力ノードでvalid: true
を探して、どのoneOf
サブスキーマが検証に合格したかを特定します。それが$ref
スキーマであることがわかります($ref
スキーマのみを含むoneOf
であるため)、つまり、その$ref
スキーマの子出力ノードは、周辺機器スキーマの出力(周辺機器スキーマの$id
URIを含む)を表します。
したがって、単一の検証パスで、それがどのような種類のサポートされている周辺機器であるか、そしてそれがどのような種類であるかを判断できます。一石二鳥です。
では、JSONスキーマで継承は可能ですか?
いいえ。
そしてはい、私たちが大丈夫であれば
基底クラスをモデル化するスキーマは、インスタンスがそのクラスの派生を表していることを検証できません。
基底クラスへの参照が必要な場合、事前に認識している派生型のみをサポートできます。
これはほとんどの人にとって許容範囲内だと思いますが、このアプローチではうまくいかないシナリオに遭遇する人が必ずいることも確信しています。
これは私がこれまで見た中で最高の継承モデリングであり、JSON Schemaが何らかの新しい機能なしに100%正しく実現することはできないとほぼ確信しています。
ポリモーフィズムのサポート方法に関する他のアイデアがある場合、またはポリモーフィズムが過大評価されており、JSON Schemaがそれをサポートする必要がないと思われる場合は、IDL Vocabularyリポジトリの会話にご参加ください。
表紙画像は、Gerd Altmann氏によるPixabayの作品です。