在本教程的第一部分中,您学习了如何使用所有可用的验证关键字来创建相当高级的架构。 许多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存储库中看到一个更完整的示例。
数据结构复杂且递归,但是描述此数据的模式非常简单:
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#" }
}
}
}
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#" }
}
}
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
有效)。 需要注意的主要事情是,schema navigation.json
引用了page.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 Repository中找到 。
示例中使用的验证器Ajv是JavaScript最快的JSON-Schema验证器。 我创建了它,因此在本教程中将使用它。 最后,我们将研究它与其他验证器的比较方式,以便您为自己选择合适的验证器。
任务
有关如何使用任务安装存储库以及测试答案的说明,请参见本教程的第1部分 。
使用“ $ ref”关键字的模式之间的引用
JSON-Schema标准允许您使用带有“ $ ref”关键字的引用来重复使用模式的重复部分。 从导航示例中可以看到,您可以引用位于的架构:
- 在另一个文件中:使用在其“ id”属性中定义的架构URI
- 在另一个文件的任何部分中:将JSON指针附加到架构引用
- 在当前模式的任何部分中:将JSON指针附加到“#”
您还可以使用等于“#”的“ $ ref”来引用整个当前架构,它允许您创建引用自己的递归架构。
因此,在我们的示例中, navigation.json
的架构是指:
- 模式
page.json
- 模式
defs.json
definitions
- 在同一模式中定义
positiveIntOrNull
page.json
的模式指的是:
- 回到schema
navigation.json
- 也可以
definitions
defs.json
文件中的defs.json
该标准要求“ $ ref”应该是对象中的唯一属性,因此,如果要除了其他模式之外还应用引用的模式,则必须使用“ allOf”关键字。
任务1
使用参考重构本教程第1部分中的用户架构。 将模式分为两个文件: user.json
和connection.json
。
将您的模式放入文件part2/task1/user.json
和part2/task1/connection.json
part2/task1/user.json
part2/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
。
约定是将引用中使用的架构的所有部分放在架构的definitions
属性内(如示例中所示)。 尽管JSON模式用于此目的保留了definitions
关键字,但是并不需要将子计划放在此处。 JSON-pointer允许您引用JSON文件的任何部分。
在URI中使用JSON指针时,应对URI中所有无效的字符进行转义(在JavaScript中,可以使用全局函数encodeURIComponent
)。
不仅可以在JSON模式中使用JSON指针。 它们可用于表示JSON数据中任何属性或项目的路径。 您可以使用库json-pointer来访问带有JSON-pointer的对象。
任务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
进行检查。
架构编号
模式通常具有具有模式URI的顶级“ id”属性。 在模式中使用“ $ ref”时 ,其值被视为相对于模式“ id”解析的URI。
解析的工作方式与浏览器解析不是绝对的URI的方式相同-它们相对于其“ id”属性中的架构URI进行解析。 如果“ $ ref”是文件名,它将替换“ id”中的文件名。 在导航示例中,导航模式ID为"http://mynet.com/schemas/navigation.json#"
,因此,当解析引用"page.json#"
时,页面模式的完整URI变为"http://mynet.com/schemas/page.json#"
(即page.json
模式的“ id” )。
如果页面模式的“ $ 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#positiveInteger"
和"defs.json#color"
"defs.json#/definitions/color"
,而不是在导航和页面模式中使用的引用"defs.json#/definitions/positiveInteger"
和"defs.json#/definitions/color"
"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" }
}
}
在很少的几行中,它变得非常混乱。 看一下示例,尝试找出哪个属性应该是字符串,哪个应该是整数。
该模式使用bar
, baz
和bax
属性定义一个对象。 属性bar
应是根据子模式有效的对象,这要求其属性foo
根据"bar"
引用有效。 由于子模式具有其自己的“ id” ,因此引用的完整URI将为"http://somewhere.else/completely.json#bar"
,因此它应该是一个整数。
现在看一下属性baz
和bax
。 它们的引用以不同的方式编写,但是它们指向相同的引用"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-Schema版本5提案
尽管这些建议尚未最终定稿为标准草案,但今天可以使用-Ajv验证程序将其实施。 它们极大地扩展了您可以使用JSON模式验证的内容,因此值得使用它们。
要将所有这些关键字与Ajv一起使用,您需要使用选项v5: true
。
关键字“常量”和“包含”
添加这些关键字是为了方便。
“常量”关键字要求数据等于关键字的值。 如果没有此关键字,则可以使用带有元素数组中一项的“ 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" }
}
}
它们都要求对象仅具有以下属性:键仅由小写字母组成,其值类型为string,并且键仅由数字组成,其值类型为number。 它们不需要任何数量的此类属性,也不限制最大数量。 这就是您可以使用“ patternGroups”进行的操作 :
{
"patternGroups": {
"^[a-z]+$": {
"minimum": 1,
"maximum": 3,
"schema": { "type": "string" }
},
"^[0-9]+$": {
"minimum": 1,
"schema": { "type": "number" }
}
}
}
上面的模式还有其他要求:应至少有一个与每个模式匹配的属性,并且不超过三个其键仅包含字母的属性。
使用“ patternProperties”无法实现相同的目的 。
限制格式化值的关键字“ formatMaximum” / “ formatMaximum”
这些关键字与“ exclusiveFormatMaximum” / “ exclusiveFormatMinimum”一起使您可以设置时间,日期以及可能具有“ format”关键字所需格式的其他字符串值的限制。
此架构要求数据是日期,并且必须大于或等于2016年1月1日:
{
"format": "date",
"formatMinimum": "2016-01-01"
}
Ajv支持将格式化数据与“日期”,“时间”和“日期时间”格式进行比较,并且您可以使用“ formatMaximum” / “ formatMaximum”关键字定义自定义格式来支持限制。
关键字“开关”
尽管所有先前的关键字都允许您更好地表达没有它们的情况,或者稍微扩展了可能性,但是它们并没有改变模式的声明性和静态性质。 此关键字使您可以使验证动态且与数据相关。 它包含多个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中,fallthrough是默认行为,而“ switch”关键字需要显式的“ continue”指令。 这是带有“ continue”指令的另一个简单示例:
"schema": {
"switch": [
{ "if": { "minimum": 10 }, "then": { "multipleOf": 2 }, "continue": true },
{ "if": { "minimum": 20 }, "then": { "multipleOf": 5 } }
]
}
如果满足第一个“如果”条件并且满足“那么”个要求,则验证将继续检查第二个条件。
“ $ data”参考
“ $ data”关键字甚至进一步扩展了JSON模式的功能,并使验证更加动态和依赖于数据。 它允许您将某些数据属性,项目或键中的值放入某些架构关键字中。
例如,此架构定义了一个具有两个属性的对象,如果同时定义了两个属性,则“更大”应大于或等于“更小”-“更小”中的值用作“更大”的最小值:
"schema": {
"properties": {
"smaller": {},
"larger": {
"minimum": { "$data": "1/smaller" }
}
}
}
Ajv为大多数非模式值的关键字实现“ $ data”引用。 如果“ $ data”引用指向错误的类型,则验证失败;如果指向未定义的值(或对象中不存在路径),则引用成功。
那么“ $ data”引用中的字符串值是什么? 它看起来类似于JSON指针,但不完全相同。 这是此标准draft定义的相对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关键字,使用两个必需的属性list
和order
定义对象的架构。 List应该是一个最多包含五个数字的数组。 所有项目均应为数字,并且应按照可以为"asc"
或"desc"
的属性order
确定的升序或降序排列。
例如,这是一个有效的对象:
{
"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-schema标准的版本5建议的新关键字。 您今天可以使用它们,但有时可能需要更多。 如果完成了任务5,您可能已经注意到,使用JSON模式很难表达一些要求。
包括Ajv在内的某些验证器允许您定义自定义关键字。 自定义关键字:
- 允许您创建无法使用JSON-Schema表示的验证方案
- 简化架构
- 帮助您将验证逻辑的大部分带入架构
- 使您的模式更具表现力,更少冗长且更接近您的应用程序域
使用Ajv的开发人员之一在GitHub上写道:
“带有自定义关键字的AJV在后端的业务逻辑验证方面为我们提供了很多帮助。 我们使用自定义关键字将一大堆控制器级别的验证整合到JSON-Schema中。 最终效果远比编写单独的验证代码好得多。”
使用自定义关键字扩展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
的对象是关键字定义。 它可以选择包含关键字适用的类型(或作为数组的类型)。 使用参数schema
和parentSchema
调用compile函数,并且应返回另一个验证数据的函数。 这使得它几乎与本地关键字一样有效,因为在编译过程中会分析模式,但是在验证过程中会产生额外的函数调用成本。
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;
}
您可以看到该函数仅返回与使用关键字maximum
和minimum
的range
关键字等效的新模式。
我们还要看看如何创建一个包含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" ]
}
}
]
}
如果你想使用带有“$数据”参考range
的关键字,你将不得不延长“V5建议”元模式包含在Ajv (见上面的链接),因此这些引用可以是数值range
和exclusiveRange
。 虽然我们的第一个实现将不支持“ $ 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);
如果将无效值传递给range
或exclusiveRange
则上面的代码将引发异常。
任务6
假设您已经定义了关键字jsonPointers
,该关键字将模式应用于由JSON指针定义的深度属性,该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
文件中查看它们的定义。
您的任务是:使用关键字jsonPointers
和requiredJsonPointers
,定义与JavaScript switch
语句相似并且具有以下语法的关键字select
( otherwise
和fallthrough
是可选的):
{
"select": {
"selector": "<relative JSON-pointer that starts from '0/'>",
"cases": [
{ "case": <value1>, "schema": { <schema1> }, "fallthrough": true },
{ "case": <value2>, "schema": { <schema2> } },
...
],
"otherwise": { <defaultSchema> }
}
}
此语法允许使用任何类型的值。 请注意, fallthrough
不同于continue
在switch
关键字。 fallthrough
将下一个案例的模式应用于数据,而无需检查选择器是否等于下一个案例的值(因为它很可能不相等)。
将您的答案放在part2/task6/select_keyword.js
和part2/task6/v5-meta-with-select.json
并运行node 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
关键字是假的。 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
。 使用此选项,验证将通过,并将具有默认值的属性添加到对象。
强制数据类型
“类型”是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-Schema验证程序
有十多个受积极支持JavaScript验证器可用。 您应该使用哪一个?
您可以在项目json-schema-benchmark中查看性能的基准以及不同的验证程序如何通过JSON-schema标准的测试套件。
一些验证器还具有一些独特的功能,可以使它们最适合您的项目。 我将在下面比较其中的一些。
is-my-json-valid和jsen
这两个验证器非常快并且具有非常简单的界面。 它们都像Ajv一样将模式编译为JavaScript函数。
它们的缺点是它们对远程引用的支持都很有限。
图式
这是一种库,其中JSON模式验证几乎是一个副作用。
它是作为通用且易于扩展的JSON模式处理器/迭代器构建的,可用于构建使用JSON模式的各种工具:UI生成器,模板等。
它已经包含了相对较快的JSON模式验证器。
但是,它根本不支持远程引用。
mis弥斯
它是快速验证器中最慢的,它具有一组全面的功能,并且对远程引用的支持有限。
它真正的亮点是对default
关键字的实现。 虽然大多数验证器对此关键字的支持有限(Ajv也不例外),但是Themis具有非常复杂的逻辑,即在复合关键字(如anyOf
内应用带有回滚的默认值。
z模式
在性能方面,这个非常成熟的验证器位于快速验证器和慢速验证器之间。 这可能是新的经过编译的验证器(以上所有和Ajv)出现之前最快的一种。
它通过了JSON模式测试套件中用于验证程序的几乎所有测试,并且具有相当全面的远程引用实现。
它具有大量选项,可让您调整许多JSON模式关键字的默认行为(例如,不接受将空数组作为数组或空字符串集),并对JSON模式施加其他要求(例如,要求minLength
关键字)用于字符串)。
我认为在大多数情况下,修改模式行为以及在JSON模式中包含对其他服务的请求都是错误的事情。 但是在某些情况下,这样做的能力大大简化了。
电视4
这是支持标准版本4的最早(也是最慢)的验证器之一。 因此,它通常是许多项目的默认选择。
如果使用它,了解它如何报告错误和丢失的引用并正确配置它非常重要,否则您将收到许多误报(即,验证通过了无效数据或未解决的远程引用)。
默认情况下不包含格式,但是它们可以作为单独的库使用。
联合会
我之所以写Ajv,是因为所有现有的验证器都是快速的或符合标准的(特别是在支持远程引用方面),但并非两者兼而有之。 Ajv填补了这一空白。
目前,它是唯一的验证者:
- 通过所有测试并完全支持远程引用
- 支持为标准版本5和
$data
参考建议的验证关键字 - 支持自定义格式和关键字的异步验证
它有选择修改的验证过程和修改验证的数据(过滤,分配缺省值和胁迫类型-见上面的例子)。
使用哪个验证器?
我认为最好的方法是尝试几种方法,然后选择最适合您的一种方法。
我写了json-schema-consolidate ,它提供了一组适配器,这些适配器统一了12个JSON-schema验证程序的接口。 使用此工具,您可以花费更少的时间在验证器之间进行切换。 我建议您在决定使用哪个验证器后将其删除,因为保留它会对性能产生负面影响。
就是这个! 我希望本教程对您有所帮助。 您已了解:
- 结构化架构
- 使用参考和ID
- 使用版本5提案中的验证关键字和$ data参考
- 异步加载远程模式
- 定义自定义关键字
- 在验证过程中修改数据
- 不同的JSON模式验证器的优缺点
谢谢阅读!
翻译自: https://code.tutsplus.com/tutorials/validating-data-with-json-schema-part-2--cms-25640