继续验证数据:使用JSON-Schema进行验证,第二部分

继续验证数据:使用JSON-Schema进行验证,第二部分

在本教程的第一部分中,您学习了如何使用所有可用的验证关键字创建相当高级的架构。许多现实世界中的 JSON 数据示例比我们的用户示例更复杂。尝试将对此类数据的所有要求放入一个文件中可能会导致非常大的架构,并且还可能有大量重复。

构建您的架构

JSON 架构标准允许您将架构分成多个部分。让我们看一下新闻站点导航的数据示例:

{
  "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"
          }
        ]
      }
    },
    ...
  ]
}

上面的导航结构有点类似于您在网站 http://dailymail.co.uk 上看到的导航结构。您可以在 GitHub 存储库中查看更完整的示例。

数据结构复杂且递归,但描述该数据的模式非常简单:

导航.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#" }
    }
  }
}

页面.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#" }
  }
}

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

查看上面的模式以及它们描述的导航数据(根据模式 navigation.json 有效)。主要要注意的是,模式 navigation.json 引用了模式 page.json,而后者又引用了第一个模式。

根据架构验证用户记录的 JavaScript 代码可能是:

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);

所有代码示例均可在 GitHub 存储库中找到。

示例中使用的验证器 Ajv 是 JavaScript 最快的 JSON 架构验证器。我创建了它,因此我将在本教程中使用它。最后我们将比较它与其他验证器的比较,以便您选择适合您的验证器。

任务

请参阅教程的第 1 部分,了解如何安装包含任务的存储库并测试您的答案。

使用“$ref”关键字的模式之间的引用

JSON-Schema 标准允许您使用带有 “$ref” 关键字的引用来重用架构的重复部分。正如您从导航示例中看到的,您可以引用位于以下位置的架构:

  • 在另一个文件中:使用在其“id”属性中定义的架构 URI
  • 在另一个文件的任何部分:将 JSON 指针附加到架构引用
  • 在当前架构的任何部分:将 JSON 指针附加到“#”

您还可以使用等于“#”的“$ref”来引用整个当前架构 - 它允许您创建引用自身的递归架构。

因此,在我们的示例中,navigation.json 中的架构指的是:

  • 架构 page.json
  • 架构中的定义 defs.json
  • 同一架构中定义 positiveIntOrNull

page.json 中的架构指的是:

  • 返回架构 navigation.json
  • 还有 definitions 文件中的 defs.json

该标准要求“$ref”应该是对象中的唯一属性,因此如果除了另一个架构之外还想应用引用的架构,则必须使用 >“allOf”关键字。

任务 1

使用参考重构本教程第 1 部分中的用户架构。将架构分为两个文件:user.jsonconnection.json

将您的架构放入文件 part2/task1/user.jsonpart2/task1/connection.json 并运行 node part2/ task1/validate 检查您的架构是否正确。

JSON 指针

JSON 指针是定义 JSON 文件各部分路径的标准。该标准在 RFC6901 中进行了描述。

该路径由用“/”字符连接的段(可以是任何字符串)组成。如果段中包含字符“~”或“/”,则应将其替换为“~0”和“~1”。每个段表示 JSON 数据中的属性或索引。

如果您查看导航示例,定义 color 属性的 “$ref” 是“defs.json#/definitions/color”,其中“defs .json#”是模式 URI,“/definitions/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-Schema进行验证,第二部分的详细内容,更多请关注编程之家其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


kindeditor4.x代码高亮功能默认使用的是prettify插件,prettify是Google提供的一款源代码语法高亮着色器,它提供一种简单的形式来着色HTML页面上的程序代码,实现方式如下: 首先在编辑器里面插入javascript代码: 确定后会在编辑器插入这样的代码: <pre
这一篇我将介绍如何让kindeditor4.x整合SyntaxHighlighter代码高亮,因为SyntaxHighlighter的应用非常广泛,所以将kindeditor默认的prettify替换为SyntaxHighlighter代码高亮插件 上一篇“让kindeditor显示高亮代码”中已经
js如何实现弹出form提交表单?(图文+视频)
js怎么获取复选框选中的值
js如何实现倒计时跳转页面
如何用js控制图片放大缩小
JS怎么获取当前时间戳
JS如何判断对象是否为数组
JS怎么获取图片当前宽高
JS对象如何转为json格式字符串
JS怎么获取图片原始宽高
怎么在click事件中调用多个js函数
js如何往数组中添加新元素
js如何拆分字符串
JS怎么对数组内元素进行求和
JS如何判断屏幕大小
js怎么解析json数据
js如何实时获取浏览器窗口大小
原生JS实现别踩白块小游戏(五)
原生JS实现别踩白块小游戏(一)