当您处理复杂的结构化数据时,您需要确定数据是否有效。JSON-Schema 是 JSON 文档的标准,描述了 JSON 数据的结构和要求。在这个由两部分组成的系列中,您将学习如何使用 JSON-Schema 来验证数据。
假设您有一个用户数据库,其中每条记录都类似于此示例:
{ "id": 64209690, "name": "Jane Smith", "email": "jane.smith@gmail.com", "phone": "07777 888 999", "address": { "street": "Flat 1, 188 High Street Kensington", "postcode": "W8 5AA", "city": "London", "country": "United Kingdom" }, "personal": { "DOB": "1982-08-16", "age": 33, "gender": "female" }, "connections": [ { "id": "35434004285760", "name": "John Doe", "connType": "friend", "since": "2014-03-25" }, { "id": 13418315, "name": "James Smith", "connType": "relative", "relation": "husband", "since": "2012-07-03" } ], "feeds": { "news": true, "sport": true, "fashion": false }, "createdAt": "2022-08-31T18:13:48.616Z" }
我们要处理的问题是如何判断像上面这样的记录是否有效。
在描述您的数据需求时,示例非常有用,但还不够。JSON-Schema 来拯救。这是描述用户记录的可能模式之一:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | { "$schema": "https://json-schema.org/draft-04/schema#", "id": "https://mynet.com/schemas/user.json#", "title": "User", "description": "User profile with connections", "type": "object", "properties": { "id": { "description": "positive integer or string of digits", "type": ["string", "integer"], "pattern": "^[1-9][0-9]*$", "minimum": 1 }, "name": { "type": "string", "maxLength": 128 }, "email": { "type": "string", "format": "email" }, "phone": { "type": "string", "pattern": "^[0-9()\-\.\s]+$" }, "address": { "type": "object", "additionalProperties": { "type": "string" }, "maxProperties": 6, "required": ["street", "postcode", "city", "country"] }, "personal": { "type": "object", "properties": { "DOB": { "type": "string", "format": "date" }, "age": { "type": "integer", "minimum": 13 }, "gender": { "enum": ["female", "male"] } } "required": ["DOB", "age"], "additionalProperties": false }, "connections": { "type": "array", "maxItems": 150, "items": { "title": "Connection", "description": "User connection schema", "type": "object", "properties": { "id": { "type": ["string", "integer"], "pattern": "^[1-9][0-9]*$", "minimum": 1 }, "name": { "type": "string", "maxLength": 128 }, "since": { "type": "string", "format": "date" }, "connType": { "type": "string" }, "relation": {}, "close": {} }, "oneOf": [ { "properties": { "connType": { "enum": ["relative"] }, "relation": { "type": "string" } }, "dependencies": { "relation": ["close"] } }, { "properties": { "connType": { "enum": ["friend", "colleague", "other"] }, "relation": { "not": {} }, "close": { "not": {} } } } ], "required": ["id", "name", "since", "connType"], "additionalProperties": false } }, "feeds": { "title": "feeds", "description": "Feeds user subscribes to", "type": "object", "patternProperties": { "^[A-Za-z]+$": { "type": "boolean" } }, "additionalProperties": false }, "createdAt": { "type": "string", "format": "date-time" } } } |
查看上面的架构及其描述的用户记录(根据此架构有效)。这里有很多解释要做。
根据架构验证用户记录的 javascript 代码可以是:
1 2 3 4 5 6 7 8 9 | const Ajv = require('ajv'); const ajv = Ajv({allErrors: true}); const valid = ajv.validate(userSchema, userdata); if (valid) { console.log('User data is valid'); } else { console.log('User data is INVALID!'); console.log(ajv.errors); } |
或为了更好的性能:
1 2 3 | const validate = ajv.compile(userSchema); const valid = validate(userData); if (!valid) console.log(validate.errors); |
如果您使用的是 ESM,请执行以下操作:
1 2 3 4 5 6 7 8 9 | import ajv from "ajv" const ajv = Ajv({allErrors: true, code: {esm: true}}); const valid = ajv.validate(userSchema, userData); if (valid) { console.log('User data is valid'); } else { console.log('User data is INVALID!'); console.log(ajv.errors); } |
什么是ESM?
ESM(ECMAScript 模块)是一种模块格式,它正在慢慢取代 CommonJS(require()由于它在 Web 浏览器和 node.js 中工作以及它提供的新功能而使用的格式。
如果您想要最现代的代码,请尝试使用 ESM。否则,如果您想坚持当前最常见的格式,只需使用 CJS。在本教程中,示例将使用 CJS,但我建议尝试使用 ESM 作为替代。
有关 ESM 以及如何在 Node.js 中使用它的更多信息,请查看这篇关于 ESM的文章。
GitHub repo tutsplus-json-schema中提供了所有代码示例。您也可以在浏览器中试用。
示例中使用的验证器Ajv是 JavaScript 最快的 JSON-Schema 验证器。我创建了它,所以我将在本教程中使用它。
在继续之前,让我们快速处理所有原因。
为什么将数据验证作为一个单独的步骤?
快速失败
避免数据损坏
简化处理代码
在测试中使用验证码
为什么选择 JSON(而不是 XML)?
与 XML 一样广泛采用
比 XML 更容易处理和更简洁
由于 javaScript 主导了 Web 开发
为什么使用模式?
声明性的
更容易维护
非编码人员也能看懂
无需编写代码,可使用第三方开源库
为什么选择 JSON 模式?
在所有 JSON 验证标准中最广泛采用
非常成熟(当前版本是4,有版本5的建议)
涵盖了大部分验证场景
为模式使用易于解析的 JSON 文档
平台无关
易于扩展
30 多个不同语言的验证器,包括 10 多个 JavaScript,因此无需自己编写代码
任务
本教程包括几个相对简单的任务,以帮助您更好地理解 JSON 模式以及如何使用它。有一些简单的 JavaScript 脚本可以检查您是否正确完成了它们。要运行它们,您需要安装 node.js(您不需要使用它)。只需安装nvm(节点版本管理器)和最新的 node.js 版本:
1 2 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash nvm install node |
您还需要克隆 repo 并运行npm install(它将安装 Ajv 验证器)。
让我们深入了解架构!
JSON 模式始终是一个对象。它的属性称为“关键字”。其中一些描述了数据的规则(例如,“类型”和“属性”),一些描述了模式本身(“$schema”、“id”、“title”、“description”)——我们将得到他们以后。
如果数据根据该模式中的所有关键字都有效,则数据根据该模式有效——这很简单。
数据属性
由于大多数 JSON 数据由具有多个属性的对象组成,因此关键字“properties”可能是最常用的关键字。它仅适用于对象(有关“应用”的含义,请参阅下一节)。
您可能已经注意到,在上面的示例中,“properties”关键字中的每个属性都描述了数据中的相应属性。
每个属性的值本身就是一个 JSON 模式——JSON 模式是一个递归标准。根据“properties”关键字中的相应模式,数据中的每个属性都应该是有效的。
这里重要的是“properties”关键字不需要任何属性。它只为数据中存在的属性定义模式。
例如,如果我们的架构是:
1 2 3 4 5 | { "properties": { "foo": { "type": "string" } } } |
那么根据此模式,具有或不具有属性“foo”的对象都可以是有效的:
1 | {foo: "bar"}, {foo: "bar", baz: 1}, {baz: 1}, {} // all valid |
并且只有属性 foo 不是字符串的对象是无效的:
1 | { foo: 1 } // invalid |
在浏览器中试试这个例子。
数据类型
您已经弄清楚关键字“类型”的作用。它可能是最重要的关键字。它的值(一个字符串或字符串数组)定义了数据必须是什么类型(或类型)才有效。
正如您在上面的示例中看到的,用户数据必须是一个对象。
大多数关键字适用于某些数据类型——例如,关键字“properties”仅适用于对象,关键字“pattern”仅适用于字符串。
“申请”是什么意思?假设我们有一个非常简单的模式:
1 | { "pattern": "^[0-9]+$" } |
您可能希望根据这样的模式有效,数据必须是匹配模式的字符串:
1 | "12345" |
但是 JSON-schema 标准规定,如果某个关键字不适用于该数据类型,则该数据根据该关键字有效。这意味着根据上述模式,任何不是“字符串”类型的数据都是有效的——数字、数组、对象、布尔值,甚至是 null。如果您只希望与模式匹配的字符串有效,则您的架构应该是:
1 2 3 4 | { "type": "string", "pattern": "^[0-9]+$" } |
因此,您可以制作非常灵活的模式来验证多种数据类型。
查看用户示例中的属性“id”。根据此架构,它应该是有效的:
1 2 3 4 5 | { "type": ["string", "integer"], "pattern": "^[1-9][0-9]*$", "minimum": 1 } |
此模式要求有效的数据应该是"string"或"integer"。还有一个关键字“pattern”只适用于字符串;它要求字符串只能由数字组成,而不是从 0 开始。关键字“minimum”仅适用于数字;它要求数量不小于1。
表达相同要求的另一种更详细的方法是:
1 2 3 4 5 6 | { "anyOf": [ { "type": "string", "pattern": "^[1-9][0-9]*$" }, { "type": "integer", "minimum": 1 }, ] } |
但是由于 JSON-schema 的定义方式,这个模式相当于第一个模式,在大多数验证器中验证更短、更快。
您可以在模式中使用的数据类型是"object"、"array"、"number"、"integer"、"string"、"boolean"和"null"。请注意,“数字”包括“整数” ——所有整数也是数字。
号码验证
有几个关键字可以验证数字。本节中的所有关键字仅适用于数字(包括整数)。
“最小”和“最大”是不言自明的。除了它们之外,还有关键字"exclusiveMinimum"和"exclusiveMaximum"。在我们的用户示例中,用户年龄必须是 13 或更大的整数。如果用户年龄的架构是:
1 2 3 4 5 | { "type": "integer", "minimum": 13, "exclusiveMinimum": true } |
那么此模式将要求用户年龄严格大于 13,即允许的最低年龄为 14。
另一个验证数字的关键字是"multipleOf"。它的名称也解释了它的作用,您可以查看JSON-schema 关键字参考以了解它是如何工作的。
字符串验证
还有几个关键字可以验证字符串。本节中的所有关键字仅适用于字符串。
“maxLength”和“minLength”要求字符串不长于或不短于给定的数字。JSON 模式标准要求将一个 unicode 对(例如表情符号字符)计为单个字符。当您访问字符串的属性时,JavaScript 将其计为两个字符.length。
一些验证器根据标准的要求确定字符串长度,而另一些则以 JavaScript 方式进行,这种方式更快。Ajv允许你指定如何确定字符串长度,默认是符合标准的。
1 2 3 4 5 6 7 8 9 | var schema = { "maxLength": 2 };
var ajv = Ajv(); // count unicode pairs as one character ajv.validate(schema, "??"); // true ajv.validate(schema, "??!"); // false
var ajv = Ajv({unicode: false}); // count unicode pairs as two characters ajv.validate(schema, "?"); // true ajv.validate(schema, "?!"); // false |
您已经看到了“模式”关键字的作用——它只要求数据字符串与根据 JavaScript 中使用的相同标准定义的正则表达式匹配。有关架构和匹配的正则表达式,请参见下面的示例:
1 2 3 | {"pattern": "^[1-9][0-9]*$" }; /^[1-9][0-9]*$/; // id {"pattern": "^[A-Za-z]+$" }; /^[a-z]+$/i; // letters {"pattern": "^[0-9()\-\.\s]+$"}; /^[0-9()\-\.\s]+$/; // phone |
“ format”关键字定义了字符串的语义验证,例如用户示例中的“email”、“date”或“date-time”。JSON-Schema 还定义了格式"uri"、"hostname"、"ipv4"和"ipv6"。验证器以不同方式定义格式,优化验证速度或正确性。Ajv 给你一个选择:
1 2 3 4 5 6 7 | var ajv = Ajv(); // "fast" format validation ajv.validate({"format": "date"}, "2015-12-24"); // true ajv.validate({"format": "date"}, "2015-14-33"); // true
var ajv = Ajv({format: 'full'); // more thorough format validation ajv.validate({"format": "date"}, "2015-12-24"); // true ajv.validate({"format": "date"}, "2015-14-33"); // false |
大多数验证器允许您将自定义格式定义为正则表达式或验证函数。我们可以为我们的模式定义一个自定义格式“电话”,以便在多个地方使用它:
1 | ajv.addFormat('phone', /^[0-9()\-\.\s]+$/); |
然后该phone属性的架构将是:
1 | { "type": "string", "format": "phone" } |
任务1
创建一个模式,要求数据是日期(字符串)或年份(数字),并且年份大于或等于 1976。
将您的答案放入文件中part1/task1/date_schema.json并运行node part1/task1/validate以检查它。
对象验证
除了"properties"之外,您还可以在我们的用户示例中看到适用于对象的其他几个关键字。
“必需”关键字列出了对象中必须存在的属性才能使其有效。正如你所记得的,“properties”关键字不需要属性,它只在它们存在时验证它们。"required"补充了"properties",允许您定义哪些属性是必需的,哪些是可选的。
如果我们有这个模式:
1 2 3 4 5 6 | { "properties": { "foo": { "type": "string" } }, "required": ["foo"] } |
那么所有没有属性foo的对象都是无效的。
请注意,这个模式仍然不要求我们的数据是一个对象——根据它,所有其他数据类型都是有效的。为了要求我们的数据是一个对象,我们必须给它添加“type”关键字。
在浏览器中尝试上面的示例。
“ patternProperties”关键字允许您定义模式,根据该模式,如果属性名称与正则表达式匹配,则数据属性值应该是有效的。它可以与同一模式中的“properties”关键字结合使用。
根据此架构,用户示例中的 feeds 属性应该是有效的:
1 2 3 4 5 6 7 | { "type": "object", "patternProperties": { "^[A-Za-z]+$": { "type": "boolean" } }, "additionalProperties": false } |
为了有效,feeds它应该是一个具有名称仅由拉丁字母组成且值为布尔值的属性的对象。
" additionalProperties"关键字允许您定义架构,根据该架构,所有其他关键字(未在"properties"中使用且不匹配"patternProperties")应该是有效的,或者完全禁止其他属性,就像我们在feeds属性架构中所做的那样以上。
在以下示例中,“additionalProperties”用于为特定范围内的整数哈希创建一个简单的模式:
01 02 03 04 05 06 07 08 09 10 11 12 13 | var schema = { "type": "object", "additionalProperties" { "type": "integer", "minimum": 0, "maximum": 65535 } }; var validate = ajv.compile(schema); validate({a:1,b:10,c:100}); // true validate({d:1,e:10,f:100000}); // false validate({g:1,h:10,i:10.5}); // false validate({j:1,k:10,l:'abc'}); // false |
“ maxProperties”和“minProperties”关键字允许您限制对象中的属性数量。在我们的用户示例中,该address属性的架构是:
1 2 3 4 5 6 | { "type": "object", "additionalProperties": { "type": "string" }, "maxProperties": 6, "required": ["street", "postcode", "city", "country"] } |
此模式要求地址是具有必需属性street、和的对象postcode,允许两个附加属性(“maxProperties”为 6),并要求所有属性都是字符串。citycountry
“dependencies”可能是最复杂、最令人困惑和最不常用的关键字,但同时也是一个非常强大的关键字。如果数据具有某些属性,它允许您定义数据应满足的要求。
对对象有两种这样的要求:具有某些其他属性(称为“属性依赖”)或满足某些模式(“模式依赖”)。
在我们的用户示例中,用户连接应该有效的可能模式之一是:
1 2 3 4 5 6 7 8 9 | { "properties": { "connType": { "enum": ["relative"] }, "relation": { "type": "string" } }, "dependencies": { "relation": ["close"] } } |
它要求connType属性等于“relative”(参见下面的“enum”关键字),并且如果relation属性存在,它是一个字符串。
它不需要relation存在,但“依赖项”关键字要求如果relation属性存在,那么该close属性也应该存在。
我们的模式中没有为close属性定义验证规则,尽管从示例中我们可以看到它可能必须是布尔值。我们可以纠正这个遗漏的方法之一是将“依赖”关键字更改为使用“模式依赖”:
1 2 3 4 5 6 7 8 | "dependencies": { "relation": { "properties": { "close": { "type": "boolean" }, }, "required": ["close"] } } |
您可以在浏览器中使用更新后的用户示例。
请注意,“dependencies”关键字中“relation”属性中的模式用于验证父对象(即连接),而不是relation数据中属性的值。
任务 2
您的数据库包含人和机器。仅使用到目前为止我已经解释过的关键字来创建一个模式来验证它们。一个样本人类对象:
1 2 3 4 5 6 | { "human": true, "name": "Jane", "gender": "female", "DOB": "1985-08-12" } |
示例机器对象:
1 2 3 4 | { "model": "TX1000", "made": "2013-08-29" } |
请注意,它应该是一个模式来验证人和机器,而不是两个模式。
将您的答案放入文件中part1/task2/human_machine_schema.json并运行node part1/task2/validate以检查它。
提示:使用“dependencies”关键字,并在文件part1/task2/invalid.json中查看哪些对象应该是无效的。
哪些可能也应该无效的对象不在invalid.json文件中?
此任务的主要内容是验证的目的不仅仅是验证所有有效对象是否有效。我多次听到这个论点:“这个模式验证我的有效数据是有效的,因此它是正确的。” 这个论点是错误的,因为你不需要做太多事情来实现它——一个空的模式就可以完成这项工作,因为它验证任何数据都是有效的。
我认为验证的主要目的是将无效数据验证为无效,这就是所有复杂性的来源。
数组验证
有几个关键字可以验证数组(它们仅适用于数组)。
“maxItems”和“minItems”要求数组的项目数不超过(或不少于)一定数量。在用户示例中,模式要求连接数不超过 150。
“ items”关键字定义了一个模式(或多个模式),根据该模式,项目应该是有效的。如果这个关键字的值是一个对象(如在用户示例中),那么这个对象是一个模式,根据该模式数据应该是有效的。
如果“items”关键字的值是一个数组,那么这个数组包含模式,根据这些模式,相应的项目应该是有效的:
1 2 3 4 5 6 7 | { "type": "array", "items": [ { "type": "integer" }, { "type": "string" } ] } |
上面简单示例中的模式要求数据是一个数组,第一项是整数,第二项是字符串。
这两个之后的项目呢?上面的模式没有定义对其他项目的要求。它们可以用“additionalItems”关键字定义。
“ additionalItems”关键字仅适用于“items”关键字为数组且数据中的item多于“items”关键字的情况。在所有其他情况下(没有“items”关键字,它是一个对象,或者数据中没有更多项目),“additionalItems”关键字将被忽略,无论其值如何。
如果“additionalItems”关键字为真,则简单地忽略它。如果它是 false 并且数据中的项目多于“items”关键字,则验证失败:
01 02 03 04 05 06 07 08 09 10 11 | const schema = { "type": "array", "items": [ { "type": "integer" }, { "type": "string" } ], "additionalItems": false };
const validate = ajv.compile(schema); console.log(validate([1, "foo", 3])); // false |
如果“additionalItems”关键字是一个对象,那么这个对象就是一个模式,根据该模式,所有附加项目都应该是有效的:
01 02 03 04 05 06 07 08 09 10 11 12 13 | const schema = { "type": "array", "items": [ { "type": "integer" }, { "type": "string" } ], "additionalItems": { "type": "integer" } };
const validate = ajv.compile(schema); console.log(validate([1, "foo", 3])); // true console.log(validate([1, "foo", 3, 4])); // true console.log(validate([1, "foo", "bar"])); // false |
请尝试这些示例以了解“项目”和“附加项目”如何工作。
适用于数组的最后一个关键字是"uniqueItems"。如果它的值为true,它只要求数组中的所有项目都是不同的。
验证关键字“uniqueItems”的计算成本可能很高,因此一些验证器选择不实现它或仅部分实现它。
Ajv 可以选择忽略此关键字:
01 02 03 04 05 06 07 08 09 10 11 12 | const schema = { "type": "array", "uniqueItems": true };
const ajv = Ajv(); // validate uniqueItems ajv.validate(schema, [1, 2, 3]); // true ajv.validate(schema, [1, 2, 2]); // false
const ajv = Ajv({uniqueItems: false}); // ignore uniqueItems ajv.validate(schema, [1, 2, 3]); // true ajv.validate(schema, [1, 2, 2]); // true |
任务 3
在 JavaScript 中创建日期对象的一种方法是将 2 到 7 个数字传递给 Date 构造函数:
1 2 | const date = new Date(2015, 2, 15); // Sun Mar 15 2015 00:00:00 GMT+0000, month is 0 based const date2 = new Date(year, month0, day, hour, minute, seconds, ms); |
你有一个数组。创建一个模式,该模式将验证这是 Date 构造函数的有效参数列表。
将您的答案放入文件中part1/task3/date_args_schema.json并运行node part1/task3/validate以检查它。
“枚举”关键字
“枚举”关键字要求数据等于几个值之一。它适用于所有类型的数据。
在用户示例中,它用于将gender属性内部的personal属性定义为“男性”或“女性”。它还用于定义connType用户连接中的属性。
“ enum”关键字可以用于任何类型的值,不仅是字符串和数字,尽管它不是很常见。
它还可以用于要求数据等于特定值,如用户示例中所示:
1 2 3 4 | "properties": { "connType": { "enum": ["relative"] }, ... } |
最新版本的 JSON Schema 支持的另一个关键字是 constant:
1 2 3 4 5 6 7 8 | const schema = { "constant": "relative" };
const ajv = Ajv({v5: true}); // this options enables v5 keywords const validate = ajv.compile(schema); validate("relative"); // true validate("other"); // false |
复合验证关键字
有几个关键字允许您定义涉及针对多个模式进行验证的高级逻辑。本节中的所有关键字都适用于所有数据类型。
我们的用户示例使用“oneOf”关键字来定义对用户连接的要求。如果数据成功验证了数组中的一个模式,则此关键字有效。
如果数据根据“oneOf”关键字中的所有模式无效或根据两个或多个模式有效,则数据无效。
让我们更仔细地看一下我们的示例:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 | { ... "oneOf": [ { "properties": { "connType": { "enum": ["relative"] }, "relation": { "type": "string" } }, "dependencies": { "relation": ["close"] } }, { "properties": { "connType": { "enum": ["friend", "colleague", "other"] }, "relation": { "not": {} }, "close": { "not": {} } } } ], ... } |
上面的模式要求用户连接要么是“相对的”(connType属性),在这种情况下,它可能具有属性relation(字符串)和close(布尔值),或者是“朋友”、“同事”或“其他”类型之一(其中如果它不能有属性relation和close)。
这些用户连接模式是互斥的,因为没有数据可以同时满足它们。因此,如果连接有效且类型为“相对”,则针对第二个模式验证它是没有意义的——它总是无效的。尽管如此,任何验证器都将始终针对两种模式验证数据,以确保它仅根据一种模式有效。
还有另一个关键字可以让您避免它:“anyOf”。此关键字仅要求数据根据数组中的某些模式(可能对多个模式)有效。
在上述情况下,模式是互斥的并且没有数据可以根据多个模式有效,最好使用“anyOf”关键字 - 在大多数情况下它会更快地验证(除了一个,其中数据根据最后一个模式有效)。
在“anyOf”同样出色的情况下使用“oneOf”是一个非常常见的错误,会对验证性能产生负面影响。
我们的用户示例也将受益于将"oneOf"替换为"anyOf"。
但是,在某些情况下,我们确实需要“oneOf”关键字:
1 2 3 4 5 6 7 | { "type": "string", "oneOf": [ { "pattern": "apple" } { "pattern": "orange" } ] } |
上面的模式将成功验证提到橘子或苹果的字符串,但不能同时验证两者(并且确实存在可以同时提到两者的字符串)。如果这就是您所需要的,那么您需要使用"oneOf"。
与布尔运算符相比,“anyOf”类似于布尔 OR,“oneOf”类似于 XOR(异或)。JavaScript(和许多其他语言)没有为异或定义运算符这一事实表明它很少需要。
还有关键字“allOf”。它要求数据根据数组中的所有模式都是有效的:
1 2 3 4 5 6 | { "allOf": [ { "$ref": "http://mynet.com/schemas/feed.json#" }, { "maxProperties": 5 } ] } |
“ $ref”关键字允许您根据另一个文件(或其中的某些部分)中的架构要求数据有效。我们将在本教程的第二部分中研究它。
另一个错误是在"oneOf"、"anyOf"和"allOf"关键字中放置了超过绝对必要的模式。例如,在我们的用户示例中,我们可以将连接应满足的所有要求放入“anyOf”中。
我们也可以用苹果和橙子不必要地复杂化这个例子:
1 2 3 4 5 6 | { "oneOf": [ { "type": "string", "pattern": "apple" } { "type": "string", "pattern": "orange" } ] } |
另一个“逻辑”关键字是“不”。它要求数据根据作为此关键字值的架构是无效的。
例如:
1 2 3 4 5 6 | { "type": "string", "not": { "pattern": "apple" } } |
上面的模式要求数据是一个不包含“apple”的字符串。
在用户示例中,“not”关键字用于防止某些属性在“oneOf”中的一种情况下使用,尽管它们已定义:
1 2 3 4 5 6 7 | { "properties": { "connType": { "enum": ["friend", "colleague", "other"] }, "relation": { "not": {} }, "close": { "not": {} } } } |
上例中“not”关键字的值是一个空模式。空模式将验证任何值是否有效,而“not”关键字将使其无效。relation因此,如果对象具有属性或,架构验证将失败close。您可以使用“not”和“required”关键字的组合来实现相同的目的。
“not”关键字的另一个用途是定义要求数组包含根据某些模式有效的项目的模式:
01 02 03 04 05 06 07 08 09 10 | { "not": { "items": { "not: { "type": "integer", "minimum": 5 } } } } |
上面的模式要求数据是一个数组,并且它至少包含一个大于或等于 5 的整数项。
V5 提案包含关键字“包含”以满足此要求。
01 02 03 04 05 06 07 08 09 10 11 12 | var schema = { "type": "array", "contains": { "type": "integer", "minimum": 5 } };
const ajv = Ajv({v5: true}); // enables v5 keywords const validate = ajv.compile(schema); validate([3, 4, 5]); // true validate([1, 2, 3]); // false |
任务 4
您有一个用户数据库,这些用户都与用户示例中的模式匹配。创建一个模式,根据该模式,只有满足所有这些条件的用户才有效:
21 岁以下或 60 岁以上的未婚男性
有 5 个或更少的连接
订阅 3 个或更少的订阅源
将您的答案放入文件中part1/task4/filter_schema.json并运行node part1/task4/validate以检查它。
测试数据已简化,因此请不要在架构中使用“必需”关键字。
描述模式的关键字
用户示例中使用的一些关键字不直接影响验证,但它们描述了模式本身。
" $schema"关键字定义了模式的元模式的 URI。模式本身是一个 JSON 文档,可以使用 JSON 模式进行验证。定义任何 JSON 模式的 JSON 模式称为元模式。JSON-schema 标准草案 2020-12 的元模式的 URI 。
如果您扩展标准,建议您使用“$schema”属性的不同值。
“id”是架构 URI。它可用于使用“$ref”关键字从另一个模式引用模式(或其中的一部分)——请参阅教程的第二部分。大多数验证器,包括 Ajv,都允许任何字符串作为"id"。根据标准,模式 id 应该是可用于下载模式的有效 URI。
您还可以使用“标题”和“描述”来描述架构。在验证期间不使用它们。这两个关键字都可以在模式内的任何级别上使用来描述它的某些部分,就像在用户示例中所做的那样。
最终任务
创建一个用户记录示例,当使用示例用户架构进行验证时,该示例将有 8 个或更多错误。
将您的答案放入文件中part1/task5/invalid_user.json并运行node part1/task5/validate以检查它。
我们的用户模式还有什么问题?
额外部分:JSON Schema 的替代方案
您可能已经注意到 JSON Schema 并不是 Ajv 支持的唯一模式格式。Ajv 还支持JSON 类型定义,这是一种更简单、更简洁的 JSON Schema 替代方案。例如,如果我们尝试使用 JSON Typedef描述符合JSON Patch的补丁语句,则架构将如下所示:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | { "definitions": { "addop": { "properties": { "op": { "type": "string", "enum": [ "add", "replace", "test" ] }, "value": {}, "path": { "type": "string" } } }, "removeop": { "properties": { "op": { "type": "string", "enum": [ "remove" ] }, "path": { "type": "string" } } }, "moveop": { "properties": { "op": { "type": "string", "enum": [ "move", "copy" ] }, "path": { "type": "string" }, "from": { "type": "string" } } } }, "elements": { "discriminator": "op", "mapping": { "add": { "ref": "addop" }, "replace": { "ref": "addop" }, "test": { "ref": "addop" }, "move": { "ref": "moveop" }, "copy": { "ref": "moveop" } } } } |
然后,要使用 Ajv 使用架构验证数据,我会这样做:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | "use strict";
if (process.env.NODE_ENV != "test") return;
const Ajv = require("ajv/dist/jtd"); const assert = require("assert");
const patchData = require("./data"); const patchSchema = require("./schema");
const ajv = Ajv({ allErrors: true });
const validate = ajv.compile(patchSchema); assert(test(validate));
console.log("Patch schema OK");
function test(validate) { const valid = validate(userData);
if (valid) { console.log("Patch data is valid!"); } else { console.log("Patch data is INVALID!"); console.log(validate.errors); }
return valid; } |
该代码几乎与使用 JSON Schema 相同,只是您必须导入ajv/dist/jtd.
下一步是什么?
到目前为止,您已经了解了标准定义的所有验证关键字,并且您应该能够创建相当复杂的模式。随着模式的增长,您将重用其中的某些部分。模式可以被构造成多个部分甚至多个文件以避免重复。我们将在本教程的第二部分执行此操作。
我们还将:
使用模式来定义默认值
从数据中过滤其他属性
使用 JSON 模式标准第 5 版提案中包含的关键字
定义新的验证关键字
比较现有的 JSON 模式验证器
谢谢阅读!
- 什么是ESM?
- 为什么将数据验证作为一个单独的步骤?
- 为什么选择 JSON(而不是 XML)?
- 为什么使用模式?
- 为什么选择 JSON 模式?
- 任务
- 让我们深入了解架构!
- 数据属性
- 数据类型
- 号码验证
- 字符串验证
- 任务1
- 对象验证
- 任务 2
- 数组验证
- 任务 3
- “枚举”关键字
- 复合验证关键字
- 任务 4
- 描述模式的关键字
- 最终任务
- 额外部分:JSON Schema 的替代方案
发表评论