Home > Web Front-end > JS Tutorial > body text

Moving on to validating data: Validating with JSON-Schema, Part 2

WBOY
Release: 2023-08-31 21:41:18
Original
619 people have browsed it

Moving on to validating data: Validating with JSON-Schema, Part 2

In the first part of this tutorial, you learned how to create a fairly advanced schema using all the available validation keywords. Many real-world JSON data examples are more complex than our user example. Trying to put all the requirements for this kind of data into a single file could result in a very large schema, and possibly a lot of duplication.

Build your architecture

The JSON schema standard allows you to split your schema into parts. Let’s look at a data example for news site navigation:

{
  "level": 1,
  "parent_id": null,
  "visitors": "all",
  "color": "white",
  "pages": [
    {
      "page_id": 1,
      "short_name": "home",
      "display_name": "Home",
      "url": "/home",
      "navigation": {
        "level": 2,
        "parent_id": 1,
        "color": "blue",
        "pages": [
          {
            "page_id": 11,
            "short_name": "headlines",
            "display_name": "Latest headlines",
            "url": "/home/latest",
            "navigation": {
              "level": 3,
              "parent_id": 11,
              "color": "white",
              "pages": [
                {
                  "page_id": 111,
                  "short_name": "latest_all",
                  "display_name": "All",
                  "url": "/home/latest"
                },
                ...
              ]
            }
          },
          {
            "page_id": 12,
            "short_name": "events",
            "display_name": "Events",
            "url": "/home/events"
          }
        ]
      }
    },
    ...
  ]
}
Copy after login

The navigation structure above is somewhat similar to the navigation structure you see on the website http://dailymail.co.uk. You can view a more complete example in the GitHub repository.

The data structure is complex and recursive, but the schema describing the data is very simple:

Navigation.json:

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "id": "http://mynet.com/schemas/navigation.json#",
  "title": "Navigation",
  "definitions": {
    "positiveIntOrNull": { "type": ["null", "integer"], "minimum": 1 }
  },
  "type": "object",
  "additionalProperties": false,
  "required": [ "level", "parent_id", "color", "pages" ],
  "properties": {
    "level":     { "$ref": "defs.json#/definitions/positiveInteger" },
    "parent_id": { "$ref": "#/definitions/positiveIntOrNull" },
    "visitors":  { "enum": [ "all", "subscribers", "age18" ] },
    "color":     { "$ref": "defs.json#/definitions/color" },
    "pages":     {
      "type": "array",
      "items": { "$ref": "page.json#" }
    }
  }
}
Copy after login

Page.json:

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "id": "http://mynet.com/schemas/page.json#",
  "title": "Page",
  "type": "object",
  "additionalProperties": false,
  "required": [ "page_id", "short_name", "display_name", "path" ],
  "properties": {
    "page_id":      { "$ref": "defs.json#/definitions/positiveInteger" },
    "short_name":   { "type": "string", "pattern": "^[a-z_]+$" },
    "display_name": { "type": "string", "minLength": 1 },
    "path":         { "type": "string", "pattern": "^(?:/[a-z_\-]+)+$" },
    "color":        { "$ref": "defs.json#/definitions/color" },
    "navigation":   { "$ref": "navigation.json#" }
  }
}
Copy after login

defs.json:

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

View the schemas above and the navigation data they describe (valid according to the schema navigation.json). The main thing to note is that the schema navigation.json references the schema page.json, which in turn references the first schema.

The JavaScript code to validate user records against the schema might be:

var Ajv = require('ajv');
var ajv = Ajv({
  allErrors: true,
  schemas: [
    require('./navigation.json'),
    require('./page.json'),
    require('./defs.json')
  ]
});

var validate = ajv.getSchema("http://mynet.com/schemas/navigation.json#");
var valid = validate(navigationData);
if (!valid) console.log(validate.errors);
Copy after login

All code examples can be found in the GitHub repository.

The validator Ajv used in the example is JavaScript's fastest JSON schema validator. I created it so I will use it in this tutorial. Finally we’ll compare how it compares to other validators so you can choose the one that’s right for you.

Task

See Part 1 of the tutorial to learn how to install the repository containing the tasks and test your answers.

References between patterns using the "$ref" keyword

The JSON-Schema standard allows you to reuse repeated parts of a schema using references with the "$ref" keyword. As you can see from the navigation example, you can reference the schema at:

  • In another file: use the schema URI defined in its "id"
  • attribute
  • In any part of another file: append JSON pointer to schema reference
  • In any part of the current schema: Append JSON pointer to "#"

You can also use "$ref" equal to "#" to reference the entire current schema - it allows you to create recursive schemas that reference themselves.

So, in our example, the schema in navigation.json refers to:

  • Architecture page.json
  • Definition in architecture defs.json
  • Defined in the same architecture positiveIntOrNull
The architecture in

page.json refers to:

  • Return Schema navigation.json
  • Also definitions in the file defs.json

The standard requires that "$ref" should be the only property in the object, so if you want to apply the referenced schema in addition to another schema, you must use > "allOf" Keywords.

Task 1

Use the reference to refactor the user schema from Part 1 of this tutorial. Split the schema into two files: user.json and connection.json.

Put your schema into files part2/task1/user.json and part2/task1/connection.json and run node part2/task1/validate Check if your schema is correct.

JSON pointer

JSON pointers are the standard for defining paths to various parts of a JSON file. The standard is described in RFC6901.

The path consists of segments (can be any string) connected with the "/" character. If the segment contains the characters "~" or "/", they should be replaced with "~0" and "~1". Each segment represents a property or index in the JSON data.

If you look at the navigation example, the "$ref" that defines the color property is "defs.json#/definitions/color" where "defs .json#" is the pattern URI, "/definitions/color" is a JSON pointer. It points to property color within property definitions.

The convention is to put all parts of the pattern used in refs into the definitions attribute of the pattern (as shown in the example). Although the JSON Schema standard reserves the definitions keyword for this purpose, there is no need to put the subschema there. JSON pointers allow you to reference any part of a JSON file.

When using JSON pointers in URIs, all characters that are not valid in URIs should be escaped (in JavaScript you can use the global function 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
        },
        ...
      }
    }
  }
}
Copy after login

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}$" }
      ]
    }
  }
}
Copy after login

现在代替引用 "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" }
  }
}
Copy after login

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

该架构定义了一个具有属性 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
}
Copy after login

许多 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" ]
}
Copy after login

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

将您的答案放入 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);
  });
}
Copy after login

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

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

JSON 架构版本 5 提案

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

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

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

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

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

此架构要求数据等于 1:

{ "constant": 1 }
Copy after login

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

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

{ "contains": { "type": "integer" } }
Copy after login

它相当于这个:

{
  "not": {
    "type": "array",
    "items": {
      "not": { "type": "integer" }
    }
  }
}
Copy after login

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

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

关键字“patternGroups”

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

例如架构:

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

相当于这个模式:

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

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

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

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

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

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

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

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

{
  "format": "date",
  "formatMinimum": "2016-01-01"
}
Copy after login

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

关键字“switch”

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

用一个例子更容易解释:

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

上面的架构按“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 } }
  ]
}
Copy after login

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

“$data”参考

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

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

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

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

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

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

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

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

相当于这个:

{
  "type": "object",
  "properties": {
    "date": { "format": "date" },
    "time": { "format": "time" }
  }
}
Copy after login

因为 { “$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" } }
      }
    }
  }]
}
Copy after login

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

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

任务 5

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

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

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

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

这是无效的:

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

将您的答案放入 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
}
Copy after login

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

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; }
}
Copy after login

就是这样!在此代码之后,您可以在架构中使用 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
Copy after login

传递给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;
}
Copy after login

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

让我们看看如何创建一个包含 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" ]
      } 
    }
  ]
}
Copy after login

如果您想将 “$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);
Copy after login

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

任务 6

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

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

相当于:

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

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

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

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

{
  "select": {
    "selector": "",
    "cases": [
      { "case": , "schema": {  }, "fallthrough": true },
      { "case": , "schema": {  } },
      ...
    ],
    "otherwise": {  }
  }
}
Copy after login

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

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

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

{
  "select": {
    "selector": "",
    "cases": {
      "": {  },
      "": {  },
      ...
    },
    "otherwise": {  }
  }
}
Copy after login

如果所有值都是不同的字符串并且没有 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 };
Copy after login

如果没有选项 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" }
Copy after login

如果没有选项 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 }
Copy after login

比较 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 模式验证器的接口。使用此工具,您可以减少在验证器之间切换的时间。我建议您在决定使用哪个验证器后将其删除,因为保留它会对性能产生负面影响。

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

  • Building Architecture
  • Using references and IDs
  • Using validation keywords and $data reference in version 5 proposals
  • Asynchronous loading of remote architecture
  • Define custom keywords
  • Modify data during verification
  • Advantages and Disadvantages of Different JSON Schema Validators

Thank you for reading!

The above is the detailed content of Moving on to validating data: Validating with JSON-Schema, Part 2. For more information, please follow other related articles on the PHP Chinese website!

Related labels:
source:php.cn
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template
About us Disclaimer Sitemap
php.cn:Public welfare online PHP training,Help PHP learners grow quickly!