2023年8月2日水曜日 ·8分程度で読めます

JSONスキーマの静的解析

私が初めてJSONスキーマを実装したとき、おそらくC#開発者にとって典型的なアプローチ、つまりオブジェクト指向と手続き型プログラミングを組み合わせたアプローチを採用しました。しかし、この1年ほど、どうしても頭から離れないアイデアがありました。

スキーマは、インスタンス内の既知の場所にある値に対する制約を定義します。これらの制約を何らかの方法でキャプチャし、そのようにスキーマをモデル化できたらどうなるでしょうか?そうすれば、その作業は一度だけ行えば済み、最終的にインスタンスを受け取ったときには、個々の制約を評価するだけで済みます。

私はこれを何度か試しました。確か4回です。そのたびに、うまくいくところに近づきましたが、考案した設計ではどうしても乗り越えられないという壁に常にぶつかっ ていました。

しかし、この2週間で、ついに実現しました!(そしてパフォーマンスの大幅な向上も!)

この記事では、このプロセスで学んだ、JSONスキーマ解析に関するより抽象的な事柄をいくつか共有したいと思います。これは実装者向けです!

制約とは何か?

制約はJSONスキーマの構成要素です。JSONデータ内の特定の場所に適用される個々の要件です。

このスキーマでは

データ
{ ...(以下同様、HTMLのコード部分は変更なし)

3つの制約があります

  1. ルートインスタンスはオブジェクトである必要があります
  2. fooプロパティがある場合、その値は文字列である必要があります
  3. barプロパティがある場合、その値は数値である必要があります

各制約は以下を識別します

  • スキーマ内のどこにいるか、「スキーマの場所」
  • どのようにしてそこにたどり着いたか、「評価パス」
  • インスタンスの場所
  • 1つのキーワードによる特定の要件

これらは実際にはインスタンスを必要とせず、多くの部分を事前に計算できることに注意してください。

これは設定がかなり簡単なようです。これらの制約をそれぞれモデル化し、インスタンスを取得したら、各制約をテストします。すべて合格すれば、検証は合格です。

簡単ですよね?私もそう思いました。しかし、すぐにさまざまな方法で複雑になります。

JSONスキーマを実装する際に克服すべき問題として提示しているのではなく、単に存在するメカニズムであることに注意してください。

依存関係を持つキーワード

キーワードの大部分は完全に独立して動作します。これらは、上記で見たtypepropertiesなどのキーワード、 valamint a maximumminItemstitleformat、 その他多数です。

しかし、動作するために他のキーワードに依存する(または相互作用する)キーワードがいくつかあります。 acestea sunt:

キーワード依存関係
additionalPropertiesproperties
patternProperties
containsminContains
maxContains
then
else
if
itemsprefixItems
unevaluatedItems*prefixItems
items
unevaluatedItems
unevaluatedProperties*properties
patternProperties
additionalProperties
unevaluatedProperties

* ほとんどのキーワードは兄弟要素の中からしか依存関係を見つけられませんが、unevaluated*キーワードは、兄弟要素のサブスキーマ内(同じインスタンス位置に適用される)のキーワードにも依存します。

if/then/elseキーワードは、キーワードの相互作用の良い例です。

手続き型のアプローチでは、以下のような分岐ロジックを使用します。

1if ( ... )
2then {
3  ...
4} else {
5  ...
6}

ifを評価し、その結果によってthenまたはelseを評価するかどうかが決まります。

しかし、これらを制約と考えると、これら3つは特異なブールロジックを提示します。

1valid = (if && then) || (!if && else)

個々の制約がすべてパスすれば検証がパスするという概念とは異なることに注意してください。この場合、ifがパスすると、elseはスキップされるため、パスするかどうかは関係ありません。逆に、ifが失敗すると、thenはスキップされます。

if/then/elseの相互作用は事前に計算するのは非常に簡単ですが、additionalPropertiesのような他のキーワードでは、そうはいきません。そのため、追加の複雑さが生じます。

不明なインスタンス位置

冒頭で、制約を特定の場所に適用される要件として定義しました。ただし、一部のキーワードは、サブスキーマをより一般的に適用します。これらのキーワードには以下が含まれます。

キーワードインスタンス位置
patternProperties正規表現キーのいずれかに一致するプロパティ
additionalProperties
unevaluatedProperties
unevaluatedItems
依存関係のいずれによっても評価されないオブジェクトプロパティ
contains配列内の任意の項目
items
unevaluatedItems
依存関係のいずれによっても評価されない配列項目

これらすべての場合、使用可能な場所を特定するには、インスタンスが必要です。その後初めて、制約を完成させることができます。

ここでの戦略は、「制約テンプレート」を作成することです。これは、要件と、使用可能な場所がわかったらその要件をどこに適用する必要があるかを決定するメカニズムを持つ制約です。そのため、完全な制約を作成することはできませんが、それでも少し作業を進めることができます。

静的参照

静的参照、つまり$refは、インスタンスなしで事前に解決できます。インスタンスに関係なく、常に同じドキュメント内の同じ場所を指します。簡単なモードです。場合によっては。

連結リストや二分木を検証するスキーマのように、再帰的なスキーマがある場合はどうでしょうか?このような場合、インスタンスに検証が必要なデータがなくなった場合、つまりリストの末尾またはノードの葉を読み取った場合に再帰は停止します。これを処理するには、前のセクションと同じ「制約テンプレート」アプローチを使用できます。

テンプレートソリューションは、実際には多くの場合に有効です。本当の秘訣は、いつそれを使用する必要があるかを理解することです。

動的参照

一方、動的参照は、一般的に1つのことに要約されます。動的スコープです。動的スコープとは、評価が出入りするリソースID(通常は$idによって設定される)の順序付けられたセットです。(スタックと考えてください。リソースに入るときにプッシュし、出るときにポップします。)動的スコープは、次の2つの要素の影響を受けます。

  • 評価が開始された場所
  • インスタンスデータ

ルートスキーマのダイナミクス

昨年、$dynamicRefを使用して言語でジェネリック型をモデル化する方法について説明した投稿を書きました。アイデアは次のとおりです。

  1. 未定義の「型」パラメータを識別するために、$dynamicRef$dynamicAnchorに使用して、ジェネリックスキーマを定義することから始めます。
  2. ジェネリックスキーマを参照し、独自の$dynamicAnchorを使用して「型」パラメータを定義する複数のセカンダリスキーマを定義します。型ごとに1つです。

このアプローチを使用すると、ジェネリックスキーマから評価を開始すると、「型」が定義されていないため、評価は失敗します。ただし、セカンダリスキーマから評価を開始すると、$dynamicRefの解決がセカンダリスキーマで定義されているものにリダイレクトされます。この異なる解決により、インスタンスは検証に合格できます。

これは、インスタンスが存在しなくても解決できる動的参照の良い例です。必要なのは、評価の開始点だけです。具体的には、参照ターゲットを識別するために、動的スコープがどこから始まるかを知る必要があります。

データ駆動型ダイナミクス

動的スコープが変更されるもう1つの方法は、何らかの条件付きロジックを使用することです。JSON Schema Test Suiteのこのテストは良い例です。ジェネリクスの投稿と同じアイデアを使用していますが、スキーマを個別に用意する代わりに、すべてが1つにまとめられています。

この場合、kindOfListインスタンスプロパティの値に応じて、配列内の項目は数値または文字列である必要があります。機械的には、これは、numberListまたはstringList定義のいずれかに評価を指示する一連のif/then/elseキーワードによって決定されます。どちらも$dynamicAnchor: itemTypeを定義し、$dynamicRef: #itemTypeを含むgenericList定義を参照します。

$dynamicRefが最終的にヒットしたとき、評価はnumberListまたはstringListを通過する必要がありました。これにより、どの$dynamicAnchorが解決されるかが識別されます。

この場合、インスタンスの場所がわかっても、適用する必要がある要件がわからないため、インスタンスを取得するまで制約を完全に定義することはできません。

これについて事前にできる作業を分離するための良い戦略を見つけることができませんでした。そのため、まだ評価時にこれらすべてを計算する必要があります。

概要

これらは、スキーマ評価へのアプローチを変更しようとしたときに発見した主な落とし穴でした。JSON Schemaの静的分析は、私にとって非常に興味深い研究分野であることが証明されました。そして、あなたにも興味を持っていただければ幸いです。

これらすべてをどのように実装したかについて詳しく知りたい場合は、blog.json-everything.netに概要があります。現在は手続き型よりも関数型になっていますが、それでも非常にオブジェクト指向です。

ここには、おそらくもっと多くの探求すべきことがあります。何か思いついたら、Slackで私を見つけてください。

カバー画像は、UnsplashGoogle DeepMindによるものです。