ホームページ  >  記事  >  ウェブフロントエンド  >  データの検証に進む: JSON スキーマを使用した検証、パート 2

データの検証に進む: JSON スキーマを使用した検証、パート 2

WBOY
WBOYオリジナル
2023-08-31 21:41:18755ブラウズ

データの検証に進む: JSON スキーマを使用した検証、パート 2

このチュートリアルの最初の部分では、利用可能なすべての検証キーワードを使用して、かなり高度なスキーマを作成する方法を学習しました。実際の JSON データの例の多くは、このユーザーの例よりも複雑です。この種のデータのすべての要件を 1 つのファイルにまとめようとすると、スキーマが非常に大きくなり、多くの重複が発生する可能性があります。

アーキテクチャを構築する

JSON スキーマ標準を使用すると、スキーマを部分に分割できます。ニュース サイト ナビゲーションのデータ例を見てみましょう:

リーリー

上記のナビゲーション構造は、Web サイト http://dailymail.co.uk に表示されるナビゲーション構造とある程度似ています。 GitHub リポジトリでより完全な例を参照できます。

データ構造は複雑で再帰的ですが、データを記述するスキーマは非常に単純です:

ナビゲーション.json:

リーリー

ページ.json:

リーリー

defs.json:

リーリー

上記のスキーマと、スキーマで説明されているナビゲーション データを表示します (スキーマ navigation.json に従って有効です)。主に注意すべき点は、スキーマ navigation.json がスキーマ page.json を参照し、さらにスキーマ

page.json

が最初のスキーマを参照することです。

ユーザー レコードをスキーマに対して検証する JavaScript コードは次のようになります:

リーリー

すべてのコード例は GitHub リポジトリにあります。

この例で使用されているバリデーター Ajv は、JavaScript の最速の JSON スキーマバリデーターです。作成したのでこのチュートリアルで使用します。最後に、他のバリデーターとの比較を行うので、自分に合ったバリデーターを選択できます。

###タスク###

タスクを含むリポジトリをインストールし、回答をテストする方法については、チュートリアルのパート 1 を参照してください。

「$ref」キーワードを使用したパターン間の参照

JSON-Schema 標準では、 "$ref" キーワードを使用した参照を使用して、スキーマの繰り返し部分を再利用できます。ナビゲーションの例からわかるように、スキーマは次の場所で参照できます。

    別のファイル:
  • "id" 属性で定義されたスキーマ URI を使用します。
  • 別のファイルの任意の部分: JSON ポインタをスキーマ参照に追加します
  • 現在のスキーマの任意の部分: JSON ポインタを「#」に追加します
"#" と等しい

"$ref" を使用して、現在のスキーマ全体を参照することもできます。これにより、それ自体を参照する再帰的なスキーマを作成できます。

したがって、この例では、

navigation.json のスキーマは次を参照します:

    アーキテクチャ
  • page.json
  • アーキテクチャにおける定義 defs.json<code class="inline">
  • 同じアーキテクチャで定義されています
  • positiveIntOrNull

page.json のアーキテクチャは次を参照します:

    戻りスキーマ
  • navigation.json
  • また、ファイル
  • defs.jsonsettings
標準では、

"$ref" がオブジェクト内の唯一のプロパティである必要があるため、別のスキーマに加えて参照されたスキーマを適用する場合は、 > "allOf " キーワード。 ###タスク1###

このチュートリアルのパート 1 のリファレンスを使用して、ユーザー スキーマをリファクタリングします。スキーマを

user.json

connection.json の 2 つのファイルに分割します。 スキーマをファイル part2/task1/user.json

および

part2/task1/connection.json に配置し、node part2/task1/validate## を実行します # チェックスキーマが正しい場合。 JSON ポインタ JSON ポインターは、JSON ファイルのさまざまな部分へのパスを定義するための標準です。この規格は RFC6901 に記載されています。

パスは、「/」文字で接続されたセグメント (任意の文字列) で構成されます。セグメントに文字「~」または「/」が含まれている場合は、「~0」および「~1」に置き換える必要があります。各セグメントは、JSON データ内のプロパティまたはインデックスを表します。

ナビゲーションの例を見ると、

color

プロパティを定義する

"$ref"

は "defs.json#/defitions/color" です。ここで、"defs .json# " はパターン URI、"/settings/color" は JSON ポインターです。これは、プロパティ definitions 内のプロパティ color を指します。 規則では、refs で使用されるパターンのすべての部分をパターンの definitions 属性に置きます (例に示すように)。 JSON スキーマ標準では、この目的のために

definitions

キーワードが予約されていますが、そこにサブスキーマを置く必要はありません。 JSON ポインターを使用すると、JSON ファイルの任意の部分を参照できます。 URI で JSON ポインターを使用する場合、URI で無効な文字はすべてエスケープする必要があります (JavaScript では、グローバル関数 encodeURIComponent を使用できます)。

JSON 指针不仅可以在 JSON 模式中使用。它们可用于表示 JSON 数据中任何属性或项目的路径。您可以使用库 json-pointer 来访问带有 JSON 指针的对象。

任务 2

下面的 JSON 文件描述了文件夹和文件结构(文件夹名称以“/”开头):

{
  "/": {
    "/documents": {
      "my_story~.rtf": {
        "type": "document",
        "application": ["Word", "TextEdit"],
        "size": 30476
      },
      ...
    },
    "/system": {
      "/applications": {
        "Word": {
          "type": "executable",
          "size": 1725058307
        },
        ...
      }
    }
  }
}

JSON 指针指向什么:

  • “Word”应用程序的大小,
  • “my_story~.rtf”文档的大小,
  • 可以打开“my_story~.rtf”文档的第二个应用程序的名称?

将您的答案放入 part2/task2/json_pointers.json 并运行 node part2/task2/validate 进行检查。

架构 ID

架构通常有一个顶级“id”属性,其中包含架构 URI。当在模式中使用“$ref”时,其值被视为相对于模式“id”解析的URI。

解析的工作方式与浏览器解析非绝对 URI 的方式相同 - 它们是相对于其 “id” 属性中的架构 URI 进行解析的。如果“$ref”是文件名,它将替换“id”中的文件名。在导航示例中,导航模式id为"http://mynet.com/schemas/navigation.json#",因此在引用"page.json#时" 已解析,页面架构的完整 URI 变为 "http://mynet.com/schemas/page.json#" (即“id” page.json 模式)。

如果页面架构的“$ref”是一个路径,例如"/page.json",那么它将被解析为 "http://mynet.com/page.json#"。并且 "/folder/page.json" 将被解析为 "http://mynet.com/folder/page.json#"

如果“$ref”从“#”字符开始,则将其视为哈希片段并附加到“id”中的路径(替换其中的哈希片段)。在导航示例中,引用 "defs.json#/definitions/color" 解析为 "http://mynet.com/schemas/defs.json# /definitions/color" 其中 "http://mynet.com/schemas/defs.json#" 是定义模式的 ID,"/definitions/ color" 在其内部被视为 JSON 指针。

如果“$ref”是具有不同域名的完整URI,就像链接在浏览器中的工作方式一样,它将被解析为相同的完整URI。

内部架构 ID

JSON 架构标准允许您在架构内使用 “id” 来标识这些子架构,并更改相对于内部引用进行解析的基本 URI - 这称为“更改解析”范围”。这可能是该标准中最令人困惑的部分之一,这就是为什么它不是很常用的原因。

我不建议过度使用内部 ID,但以下有一个例外,原因有二:

  • 很少有验证器在使用内部 ID 时始终遵循标准并正确解析引用(Ajv 在此完全遵循标准)。
  • 架构变得更加难以理解。

我们仍然会研究它的工作原理,因为您可能会遇到使用内部 ID 的架构,并且在某些情况下使用它们有助于构建您的架构。

首先,让我们看一下我们的导航示例。大多数引用都在 definitions 对象中,这使得引用相当长。有一种方法可以通过在定义中添加 ID 来缩短它们。这是更新后的 defs.json 架构:

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "id": "http://mynet.com/schemas/defs.json#",
  "title": "Definitions",  
  "definitions": {
    "positiveInteger": { "id": "#positiveInteger", "type": "integer", "minimum": 1 },
    "color": {
      "id": "#color",
      "anyOf": [
        { "enum": [ "red", "green", "blue", "white" ] },
        { "type": "string", "pattern": "^#(?:(?:[0-9a-fA-F]{1,2})){3}$" }
      ]
    }
  }
}

现在代替引用 "defs.json#/definitions/positiveInteger""defs.json#/definitions/color" 用于导航和页面架构,您可以使用较短的引用:"defs.json#positiveInteger""defs.json#color"。这是内部 ID 的一种非常常见的用法,因为它可以让您的参考文献更短、更易读。请注意,虽然大多数 JSON 模式验证器都可以正确处理这个简单的情况,但其中一些验证器可能不支持它。

让我们看一个更复杂的 ID 示例。以下是示例 JSON 架构:

{
  "id": "http://x.y.z/rootschema.json#",
  "definitions": {
    "bar": { "id": "#bar", "type": "string" }
  },
  "subschema": {
    "id": "http://somewhere.else/completely.json#",
    "definitions": {
      "bar": { "id": "#bar", "type": "integer" }
    },
    "type": "object",
    "properties": {
      "foo": { "$ref": "#bar" }
    }
  },
  "type": "object",
  "properties": {
    "bar": { "$ref": "#/subschema" },
    "baz": { "$ref": "#/subschema/properties/foo" },
    "bax": { "$ref": "http://somewhere.else/completely.json#bar" }
  }
}

在很少的几行内容中,它变得非常混乱。看一下示例并尝试找出哪个属性应该是字符串,哪个属性应该是整数。

该架构定义了一个具有属性 barbazbax 的对象。属性 bar 应该是根据子模式有效的对象,这要求其属性 foo 根据 "bar" 有效参考。因为子模式有自己的“id”,所以引用的完整 URI 将是 "http://somewhere.else/completely.json#bar",因此它应该是一个整数。

现在查看属性 bazbax。它们的引用以不同的方式编写,但它们指向相同的引用 "http://somewhere.else/completely.json#bar" 并且它们都应该是整数。尽管属性 baz 直接指向模式 { "$ref": "#bar" },但它仍然应该相对于子模式的 ID 进行解析,因为它就在里面。因此,根据此模式,下面的对象是有效的:

{
  "bar": { "foo": 1 },
  "baz": 2,
  "bax": 3
}

许多 JSON 模式验证器无法正确处理它,因此应谨慎使用更改解析范围的 ID。

任务 3

解决这个难题将帮助您更好地理解引用和更改分辨率范围的工作原理。您的架构是:

{
  "id": "http://x.y.z/rootschema.json#",
  "title": "Task 3",
  "description": "Schema with references - create a valid data",
  "definitions": {
    "my_data": { "id": "#my_data", "type": "integer" }
  },
  "schema1": {
    "id": "#foo",
    "allOf": [ { "$ref": "#my_data" } ]
  },
  "schema2": {
    "id": "otherschema.json",
    "definitions": {
      "my_data": { "id": "#my_data", "type": "string" }
    },
    "nested": {
      "id": "#bar",
      "allOf": [ { "$ref": "#my_data" } ]
    },
    "alsonested": {
      "id": "t/inner.json#baz",
      "definitions": {
        "my_data": { "id": "#my_data", "type": "boolean" }
      },
      "allOf": [ { "$ref": "#my_data" } ]
    }
  },
  "schema3": {
    "id": "http://somewhere.else/completely#",
    "definitions": {
      "my_data": { "id": "#my_data", "type": "null" }
    },
    "allOf": [ { "$ref": "#my_data" } ]
  },
  "type": "object",
  "properties": {
    "foo": { "$ref": "#foo" },
    "bar": { "$ref": "otherschema.json#bar" },
    "baz": { "$ref": "t/inner.json#baz" },
    "bax": { "$ref": "http://somewhere.else/completely#" },
    "quux": { "$ref": "#/schema3/allOf/0" }
  },
  "required": [ "foo", "bar", "baz", "bax", "quux" ]
}

创建一个根据此架构有效的对象。

将您的答案放入 part2/task3/valid_data.json 并运行 node part2/task3/validate 进行检查。

加载引用的架构

到目前为止,我们一直在研究相互引用的不同模式,而没有关注它们如何加载到验证器中。

一种方法是像上面的导航示例一样预加载所有连接的模式。但在某些情况下,它要么不切实际,要么不可能,例如,如果您需要使用的架构是由另一个应用程序提供的,或者您事先不知道可能需要的所有可能架构。

在这种情况下,验证器可以在验证数据时加载引用的架构。但这会使验证过程变慢。 Ajv 允许您将模式编译为验证函数,并异步加载流程中缺少的引用模式。验证本身仍然是同步且快速的。

例如,如果可以从其 ID 中的 URI 下载导航架构,则根据导航架构验证数据的代码可能如下:

var Ajv = require('ajv');
var request = require('request');
var ajv = Ajv({ allErrors: true, loadSchema: loadSchema });

var _validateNav; // validation function will be cached here once loaded and compiled

function validateNavigation(data, callback) {
  if (_validateNav) setTimeout(_validate);
  loadSchema('http://mynet.com/schemas/navigation.json', function(err, schema) {
    if (err) return callback(err);
    ajv.compileAsync(schema, function(err, v) {
      if (err) callback(err);
      else {
        _validateNav = v;
        _validate();
      }
    });
  });

  function _validate() {
    var valid = _validateNav(data);
    callback(null, { valid: valid, errors: _validateNav.errors });
  }
}


function loadSchema(uri, callback) {
  request.json(uri, function(err, res, body) {
    if (err || res.statusCode >= 400)
      callback(err || new Error('Loading error: ' + res.statusCode));
    else
      callback(null, body);
  });
}

代码定义了 validateNavigation 函数,该函数在第一次调用时加载架构并编译验证函数,并始终通过回调返回验证结果。有多种方法可以对其进行改进,从在第一次使用模式之前单独预加载和编译模式,到考虑到该函数在管理缓存模式之前可以多次调用这一事实(ajv.compileAsync 已确保架构始终仅请求一次)。

现在我们将了解为 JSON 架构标准第 5 版提议的新关键字。

JSON 架构版本 5 提案

尽管这些提案尚未最终确定为标准草案,但它们今天就可以使用——Ajv 验证器实现了它们。它们极大地扩展了您可以使用 JSON 模式验证的内容,因此值得使用它们。

要将所有这些关键字与 Ajv 一起使用,您需要使用选项 v5: true

关键字“常量”和“包含”

添加这些关键字是为了方便。

“constant”关键字要求数据等于关键字的值。如果没有此关键字,则可以通过元素数组中的一项的“enum”关键字来实现。

此架构要求数据等于 1:

{ "constant": 1 }

“contains” 关键字要求某些数组元素与该关键字中的架构相匹配。该关键字仅适用于数组;任何其他数据类型都将根据它有效。仅使用版本 4 中的关键字来表达此要求有点困难,但这是可能的。

此架构要求,如果数据是数组,则至少其中一项是整数:

{ "contains": { "type": "integer" } }

它相当于这个:

{
  "not": {
    "type": "array",
    "items": {
      "not": { "type": "integer" }
    }
  }
}

要使此模式有效,数据不应该是数组,或者它的所有项目都不应该是非整数(即某些项目应该是整数)。

请注意,如果数据是空数组,“contains” 关键字和上面的等效架构都会失败。

关键字“patternGroups”

建议将此关键字替换“patternProperties”。它允许您限制与对象中应存在的模式匹配的属性数量。 Ajv 在 v5 模式下同时支持 “patternGroups”“patternProperties”,因为第一个更加冗长,如果您不想限制属性的数量,可能更喜欢使用第二个。

例如架构:

{
  "patternGroups": {
    "^[a-z]+$": {
      "schema": { "type": "string" }
    },
    "^[0-9]+$": {
      "schema": { "type": "number" }
    }
  }
}

相当于这个模式:

{
  "patternProperties": {
    "^[a-z]+$": { "type": "string" },
    "^[0-9]+$": { "type": "number" }
  }
}

它们都要求对象仅具有以下属性:键仅包含小写字母且值类型为字符串,键值仅包含数字且值类型为数字。它们不需要任何数量的此类属性,也不限制最大数量。这就是您可以使用“patternGroups”执行的操作:

{
  "patternGroups": {
    "^[a-z]+$": {
      "minimum": 1,
      "maximum": 3,
      "schema": { "type": "string" }
    },
    "^[0-9]+$": {
      "minimum": 1,
      "schema": { "type": "number" }
    }
  }
}

上面的模式有额外的要求:至少应该有一个属性与每个模式匹配,并且键仅包含字母的属性不超过三个。

使用“patternProperties”无法实现相同的效果。

用于限制格式化值的关键字“formatMaximum” / “formatMaximum”

这些关键字与“exclusiveFormatMaximum” / “exclusiveFormatMinimum”一起允许您设置时间、日期和可能具有所需格式的其他字符串值的限制>“格式”关键字。

此架构要求数据是日期且大于或等于 2016 年 1 月 1 日:

{
  "format": "date",
  "formatMinimum": "2016-01-01"
}

Ajv 支持比较“日期”、“时间”和“日期-时间”格式的格式化数据,并且您可以定义支持 “formatMaximum” / 限制的自定义格式>“formatMaximum”关键字。

关键字“switch”

虽然之前的所有关键字要么允许您更好地表达没有它们的可能性,要么稍微扩展可能性,但它们并没有改变模式的声明性和静态性质。该关键字允许您使验证动态且依赖于数据。它包含多个 if-then 情况。

用一个例子更容易解释:

{
  "switch": [
    { "if": { "minimum": 50 }, "then": { "multipleOf": 5 } },
    { "if": { "minimum": 10 }, "then": { "multipleOf": 2 } },
    { "if": { "maximum": 4 }, "then": false }
  ]
}

上面的架构按“if”关键字中的子架构顺序验证数据,直到其中一个通过验证。当发生这种情况时,它会验证同一对象中的“then”关键字中的架构 - 这将是整个架构验证的结果。如果“then”的值为false,则验证立即失败。

这样,上面的模式要求值为:

  • 大于或等于 50 并且是 5 的倍数
  • 或 10 到 49 之间且 2 的倍数
  • 或 5 到 9 之间

这组特定的要求可以在没有 switch 关键字的情况下表达,但在更复杂的情况下这是不可能的。

任务 4

创建与上面最后一个示例等效的架构,而不使用 switch 关键字。

将您的答案放入 part2/task4/no_switch_schema.json 并运行 node part2/task4/validate 进行检查。

“switch”关键字案例还可以包含带有布尔值的“continue”关键字。如果此值为 true,则在成功的 “if” 模式匹配与成功的 “then” 模式验证后,验证将继续。这类似于 JavaScript switch 语句中的下一个情况,尽管在 JavaScript 中,fall-through 是默认行为,并且 “switch” 关键字需要显式的 “continue” 指令。这是另一个带有“继续”指令的简单示例:

"schema": {
  "switch": [
    { "if": { "minimum": 10 }, "then": { "multipleOf": 2 }, "continue": true },
    { "if": { "minimum": 20 }, "then": { "multipleOf": 5 } }
  ]
}

如果满足第一个“if”条件并且满足“then”要求,则验证将继续检查第二个条件。

“$data”参考

“$data” 关键字进一步扩展了 JSON 模式的可能性,并使验证更加动态和数据依赖。它允许您将某些数据属性、项目或键中的值放入某些架构关键字中。

例如,此模式定义了一个具有两个属性的对象,如果两个属性都定义了,“larger”应大于或等于“smaller”——“smaller”中的值用作“larger”的最小值:

"schema": {
  "properties": {
    "smaller": {},
    "larger": {
      "minimum": { "$data": "1/smaller" }
    }
  }
}

Ajv 为大多数值不是架构的关键字实现“$data”引用。如果“$data”引用指向不正确的类型,则验证失败;如果它指向未定义的值(或者对象中不存在路径),则验证成功。

那么“$data”引用中的字符串值是什么?它看起来与 JSON 指针类似,但又不完全一样。它是本标准草案定义的相对 JSON 指针。

它由一个整数组成,定义查找应遍历对象的次数(上例中的 1 表示直接父级),后跟“#”或 JSON 指针。

如果数字后跟“#”,则 JSON 指针解析的值将是属性的名称或对象具有的项目的索引。这样,“0#”代替“1/smaller”将解析为字符串“larger”,而“1#”将无效,因为整个数据不是任何对象或数组的成员。这个架构:

{
  "type": "object",
  "patternProperties": {
    "^date$|^time$": { "format": { "$data": "0#" } }
  }
}

相当于这个:

{
  "type": "object",
  "properties": {
    "date": { "format": "date" },
    "time": { "format": "time" }
  }
}

因为 { “$data”: “0#” } 被替换为属性名称。

如果指针中的数字后跟 JSON 指针,则从该数字引用的父对象开始解析此 JSON 指针。您可以在第一个“较小”/“较大”示例中看到它是如何工作的。

让我们再看看我们的导航示例。您可以在数据中看到的要求之一是页面对象中的 page_id 属性始终等于所包含的导航对象中的 parent_id 属性。我们可以使用 “$data” 引用在 page.json 模式中表达此要求:

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "id": "http://mynet.com/schemas/page.json#",
  ...
  "switch": [{
    "if": { "required": [ "navigation" ] },
    "then": {
      "properties": {
        "page_id": { "constant": { "$data": "1/navigation/parent_id" } }
      }
    }
  }]
}

添加到页面架构中的“switch”关键字要求,如果页面对象具有 navigation 属性,则 page_id 的值属性应与导航对象中 parent_id 属性的值相同。无需“switch”关键字也可以实现相同的效果,但它的表达能力较差并且包含重复:

{
  ...
  "anyOf": [
    { "not": { "required": [ "navigation" ] } },
    {
      "required": [ "navigation" ],
      "properties": {
        "page_id": { "constant": { "$data": "1/navigation/parent_id" } }
      }
    }
  ]
}

任务 5

相对 JSON 指针的示例可能会有所帮助。

使用 v5 关键字,使用两个必需属性 listorder 定义对象的架构。列表应该是一个最多包含五个数字的数组。所有项目都应该是数字,并且应按升序或降序排序,由属性 order 确定,可以是 "asc" “desc”

例如,这是一个有效的对象:

{
  "list": [ 1, 3, 3, 6, 9 ],
  "order": "asc"
}

这是无效的:

{
  "list": [ 9, 7, 3, 6, 2 ],
  "order": "desc"
}

将您的答案放入 part2/task5/schema.json 并运行 node part2/task5/validate 进行检查。

如何创建具有相同条件但大小不受限制的列表的架构?

定义新的验证关键字

我们研究了为 JSON 架构标准第 5 版提议的新关键字。您今天就可以使用它们,但有时您可能需要更多。如果您完成了任务 5,您可能已经注意到有些需求很难用 JSON 模式来表达。

一些验证器(包括 Ajv)允许您定义自定义关键字。自定义关键字:

  • 允许您创建无法使用 JSON-Schema 表达的验证场景
  • 简化您的架构
  • 帮助您将更大部分的验证逻辑引入您的架构
  • 使您的架构更具表现力、更简洁、更贴近您的应用领域

一位使用 Ajv 的开发人员在 GitHub 上写道:

> “具有自定义关键字的 ajv 在后端的业务逻辑验证方面为我们提供了很多帮助。我们使用自定义关键字将一大堆控制器级验证整合到 JSON 架构中。最终效果比编写单独的验证代码要好得多。”

在使用自定义关键字扩展 JSON 架构标准时,您必须注意的问题是架构的可移植性和理解性。您必须在其他平台上支持这些自定义关键字,并正确记录这些关键字,以便每个人都可以在您的架构中理解它们。

这里最好的方法是定义一个新的元架构,它将是草案 4 元架构或“v5 提案”元架构的扩展,其中将包括附加关键字的验证及其描述。然后,使用这些自定义关键字的架构必须将 $schema 属性设置为新元架构的 URI。

既然您已收到警告,我们将深入研究并使用 Ajv 定义几个自定义关键字。

Ajv 提供了四种定义自定义关键字的方法,您可以在文档中看到。我们将看看其中两个:

  • 使用将架构编译为验证函数的函数
  • 使用宏函数获取您的架构并返回另一个架构(带或不带自定义关键字)

让我们从范围关键字的简单示例开始。范围只是最小和最大关键字的组合,但如果您必须在架构中定义许多范围,特别是如果它们具有独占边界,则可能很容易变得无聊。

架构应该是这样的:

{
  "range": [5, 10],
  "exclusiveRange": true
}

当然,其中独占范围是可选的。定义该关键字的代码如下:

ajv.addKeyword('range', { type: 'number', compile: compileRange });
ajv.addKeyword('exclusiveRange'); // this is needed to reserve the keyword

function compileRange(schema, parentSchema) {
  var min = schema[0];
  var max = schema[1];

  return parentSchema.exclusiveRange === true
          ? function (data) { return data > min && data < max; }
          : function (data) { return data >= min && data <= max; }
}

就是这样!在此代码之后,您可以在架构中使用 range 关键字:

var schema = {
  "range": [5, 10],
  "exclusiveRange": true
};

var validate = ajv.compile(schema);

console.log(validate(5)); // false
console.log(validate(5.1)); // true
console.log(validate(9.9)); // true
console.log(validate(10)); // false

传递给addKeyword 的对象是一个关键字定义。它可以选择包含关键字适用的类型(或数组类型)。使用参数 schemaparentSchema 调用编译函数,并应返回另一个验证数据的函数。这使得它几乎与本机关键字一样高效,因为架构在编译期间进行了分析,但在验证期间存在额外函数调用的成本。

Ajv 允许您使用返回代码(作为字符串)的关键字来避免这种开销,该代码将成为验证函数的一部分,但它非常复杂,因此我们不会在这里讨论它。更简单的方法是使用宏关键字 - 您必须定义一个函数来获取该架构并返回另一个架构。

下面是 range 关键字的宏函数实现:

ajv.addKeyword('range', { type: 'number', macro: macroRange });

function macroRange(schema, parentSchema) {
  var resultSchema = {
    "minimum": schema[0],
    "maximum": schema[1]
  };

  if (parentSchema.exclusiveRange === true) {
    resultSchema.exclusiveMimimum = resultSchema.exclusiveMaximum = true;
  }

  return resultSchema;
}

您可以看到该函数只是返回与 range 关键字等效的新架构,该关键字使用关键字 maximumminimum

让我们看看如何创建一个包含 range 关键字的元架构。我们将使用草案 4 元架构作为起点:

{
  "id": "http://mynet.com/schemas/meta-schema-with.range.json#",
  "$schema": "http://json-schema.org/draft-04/schema#",
  "allOf": [
    { "$ref": "http://json-schema.org/draft-04/schema#" },
    {
      "properties": {
        "range": {
          "description": "1st item is minimum, 2nd is maximum",
          "type": "array",
          "items": [ { "type": "number" }, { "type": "number" } ],
          "additionalItems": false 
        },
        "exclusiveRange": {
          "type": "boolean",
          "default": false
        }
      },
      "dependencies": {
        "exclusiveRange": [ "range" ]
      } 
    }
  ]
}

如果您想将 “$data” 引用与 range 关键字一起使用,则必须扩展 Ajv 中包含的“v5proposal”元模式(请参阅上面的链接),以便这些引用可以是 rangeexclusiveRange 的值。虽然我们的第一个实现不支持 “$data” 引用,但具有宏功能的第二个实现将支持它们。

现在您已经有了元架构,您需要将其添加到 Ajv 并使用 range 关键字在架构中使用它:

ajv.addMetaSchema(require('./meta-schema-with-range.json'));

var schema = {
  "$schema": "http://mynet.com/schemas/meta-schema-with-range.json#",
  "range": [5, 10],
  "exclusiveRange": true
};

var validate = ajv.compile(schema);

如果将无效值传递给 rangeexclusiveRange,上面的代码将引发异常。

任务 6

假设您已经定义了关键字 jsonPointers ,它将架构应用于由 JSON 指针定义的深层属性,这些属性指向从当前指针开始的数据。此关键字与 switch 关键字一起使用非常有用,因为它允许您定义深层属性和项目的要求。例如,此架构使用 jsonPointers 关键字:

{
  "jsonPointers": {
    "0/books/2/title": { "pattern": "json|Json|JSON" },
  }
}

相当于:

  {
    "properties": {
      "books": {
        "items": [
          {},
          {},
          {
            "properties": {
              "title": { "pattern": "json|Json|JSON" }
            }
          }
        ]
      }
    }
  }

假设您还定义了关键字 requiredJsonPointers ,其工作方式与 required 类似,但使用 JSON 指针而不是属性。

如果你愿意,你也可以自己定义这些关键字,或者你可以在文件 part2/task6/json_pointers.js 中查看它们是如何定义的。

您的任务是:使用关键字 jsonPointersrequiredJsonPointers,定义类似于 JavaScript select >switch 语句并具有以下语法(否则 fallthrough 是可选的):

{
  "select": {
    "selector": "<relative JSON-pointer that starts from '0/'>",
    "cases": [
      { "case": <value1>, "schema": { <schema1> }, "fallthrough": true },
      { "case": <value2>, "schema": { <schema2> } },
      ...
    ],
    "otherwise": { <defaultSchema> }
  }
}

此语法允许任何类型的值。请注意,fallthroughswitch 关键字中的 switch 不同。 fallthrough 将下一个案例的模式应用于数据,而不检查选择器是否等于下一个案例的值(因为它很可能不相等)。

将您的答案放入 part2/task6/select_keyword.jspart2/task6/v5-meta-with-select.json 并运行 节点part2/task6/validate来检查它们。

奖励 1:改进您的实现以支持此语法:

{
  "select": {
    "selector": "<relative JSON-pointer that starts from '0/'>",
    "cases": {
      "<value1>": { <schema1> },
      "<value2>": { <schema2> },
      ...
    },
    "otherwise": { <defaultSchema> }
  }
}

如果所有值都是不同的字符串并且没有 fallthrough 则可以使用。

奖励 2:扩展“v5 提案”元架构以包含此关键字。

JSON 模式的其他用法

除了验证数据之外,JSON 模式还可用于:

  • 生成用户界面
  • 生成数据
  • 修改数据

如果您有兴趣,可以查看生成 UI 和数据的库。我们不会探讨它,因为它超出了本教程的范围。

我们将考虑使用 JSON 模式在验证数据时修改数据。

过滤数据

验证数据时的常见任务之一是从数据中删除附加属性。这允许您在将数据传递到处理逻辑之前清理数据,而不会导致模式验证失败:

var ajv = Ajv({ removeAdditional: true });

var schema = {
  "type": "object",
  "properties": {
    "foo": { "type": "string" }
  },
  "additionalProperties": false
};

var validate = ajv.compile(schema);

var data: { foo: 1, bar: 2 };

console.log(validate(data)); // true
console.log(data); // { foo: 1 };

如果没有选项 removeAdditional,验证将会失败,因为模式不允许有一个附加属性 bar。使用此选项,验证将通过并且该属性将从对象中删除。

removeAdditional 选项值为 true 时,仅当 additionalProperties 关键字为 false 时才会删除附加属性。 Ajv 还允许您删除所有附加属性,无论 additionalProperties 关键字或验证失败的附加属性(如果 additionalProperties 关键字是架构)。请查看 Ajv 文档以获取更多信息。

为属性和项指定默认值

JSON 架构标准定义了关键字“default”,该关键字包含数据应具有的值(如果未在验证数据中定义该值)。 Ajv 允许您在验证过程中分配此类默认值:

var ajv = Ajv({ useDefaults: true });

var schema = {
  "type": "object",
  "properties": {
    "foo": { "type": "number" },
    "bar": { "type": "string", "default": "baz" }
  },
  "required": [ "foo", "bar" ]
};

var data = { "foo": 1 };

var validate = ajv.compile(schema);

console.log(validate(data)); // true
console.log(data); // { "foo": 1, "bar": "baz" }

如果没有选项 useDefaults,验证将会失败,因为验证的对象中没有必需的属性 bar 。使用此选项,验证将通过,并且具有默认值的属性将添加到对象中。

强制数据类型

“type” 是 JSON 模式中最常用的关键字之一。当您验证用户输入时,从表单获取的所有数据属性通常都是字符串。 Ajv 允许您将数据强制为架构中指定的类型,以便通过验证并在之后使用正确类型的数据:

var ajv = Ajv({ coerceTypes: true });
var schema = {
  "type": "object",
  "properties": {
    "foo": { "type": "number" },
    "bar": { "type": "boolean" }
  },
  "required": [ "foo", "bar" ]
};

var data = { "foo": "1", "bar": "false" };

var validate = ajv.compile(schema);

console.log(validate(data)); // true
console.log(data); // { "foo": 1, "bar": false }

比较 JavaScript JSON 模式验证器

有十多个可用的积极支持的 JavaScript 验证器。您应该使用哪一个?

您可以在项目 json-schema-benchmark 中查看性能基准以及不同验证器如何通过 JSON-schema 标准的测试套件。

一些验证器还具有独特的功能,可以使它们最适合您的项目。我将在下面比较其中一些。

is-my-json-valid 和 jsen

这两个验证器速度非常快并且接口非常简单。它们都将模式编译为 JavaScript 函数,就像 Ajv 所做的那样。

它们的缺点是它们对远程引用的支持都很有限。

图式龙

这是一个独一无二的库,其中 JSON 模式验证几乎是一种副作用。

它被构建为通用且易于扩展的 JSON 模式处理器/迭代器,您可以使用它来构建使用 JSON 模式的各种工具:UI 生成器、模板等。

它已经包含了相对快速的 JSON 模式验证器。

不过,它根本不支持远程引用。

忒弥斯

它是快速验证器组中最慢的,它具有一套全面的功能,但对远程引用的支持有限。

它真正的亮点是它对 default 关键字的实现。虽然大多数验证器对此关键字的支持有限(Ajv 也不例外),但 Themis 具有非常复杂的逻辑,即在复合关键字(例如 anyOf)内应用默认值和回滚。

z 架构

就性能而言,这个非常成熟的验证器处于快速验证器和慢速验证器之间的边界。在新型编译验证器出现之前,它可能是最快的验证器之一(以上所有内容和 Ajv)。

它通过了验证器 JSON 模式测试套件中的几乎所有测试,并且对远程引用的实现相当彻底。

它有大量选项,允许您调整许多 JSON 模式关键字的默认行为(例如,不接受空数组作为数组或空字符串集)并对 JSON 模式施加额外要求(例如,需要minLength 关键字(字符串)。

我认为在大多数情况下,修改架构行为以及在 JSON 架构中包含对其他服务的请求都是错误的做法。但在某些情况下,这样做的能力会大大简化。

电视4

这是支持该标准第 4 版的最古老(也是最慢)的验证器之一。因此,它通常是许多项目的默认选择。

如果您正在使用它,了解它如何报告错误和丢失引用并正确配置它非常重要,否则您将收到许多误报(即,验证通过时包含无效数据或未解析的远程引用) .

默认情况下不包含格式,但它们可以作为单独的库提供。

Ajv

我编写 Ajv 是因为所有现有验证器要么速度快,要么符合标准(特别是在支持远程引用方面),但不是两者兼而有之。 Ajv 填补了这一空白。

目前它是唯一的验证器:

  • 通过所有测试并完全支持远程引用
  • 支持为标准版本 5 和 $data 参考建议的验证关键字
  • 支持自定义格式和关键字的异步验证

它具有修改验证过程和修改验证数据的选项(过滤、分配默认值和强制类型 - 请参阅上面的示例)。

使用哪个验证器?

我认为最好的方法是尝试多种方法并选择最适合您的方法。

我编写了 json-schema-consolidate,它提供了一组适配器,统一了 12 个 JSON 模式验证器的接口。使用此工具,您可以减少在验证器之间切换的时间。我建议您在决定使用哪个验证器后将其删除,因为保留它会对性能产生负面影响。

就是这个了!我希望本教程有用。您已了解:

  • 建物のアーキテクチャ
  • 参照と ID の使用
  • バージョン 5 プロポーザルでの検証キーワードと $data 参照の使用
  • リモート アーキテクチャの非同期読み込み
  • カスタムキーワードを定義する
  • 検証中にデータを変更する
  • さまざまな JSON スキーマバリデーターの長所と短所
###読んでくれてありがとう!

以上がデータの検証に進む: JSON スキーマを使用した検証、パート 2の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。