原创
鲜正权 / 叫叫技术团队
前言
这次需求中,需要对JSON数据里面的字段进行判断(互斥、共存),比如:数据中有2个字段 a、b,那么互斥就是:有a就不能有b,有b就不能有a,它俩只能存在一个。共存:a、b必须同时存在,有a就必须有b,有b就必须有a。那么,我们能实现这个需求呢?答案是:能!
一、什么是JSON-Schema
JSON Schema 是用于验证 JSON 数据结构的强大工具,Schema可以理解为模式或者规则。
我们这边只做简要的介绍,详情请参阅:
- What is a schema? --- Understanding JSON Schema 2020-12 documentation
- JSON Schema 规范(中文版) - JSON Schema 规范(中文版)
- JSON Schema
二、前置条件
要实现这次的需求,我们需要用到 JSON Schema 的 组合 、 条件应用 以及 ajv-keywords 库。 本文需要对 JSON Schema 有一定的了解。 本文中所有校验都是使用的 ajv 库
组合
我们需要使用到组合里面的
allOf
、anyOf
、not
。详情参阅:JSON Schema 规范(中文版) - JSON Schema 规范(中文版)
- allOf: (AND) 必须对_所有_子模式有效。(给定的数据必须针对给定的所有子模式有效。)
- anyOf: (OR) 必须对_任何子_模式有效。(数据必须满足任意一个或多个给定子模式。)
- not: (NOT)_不能_对给定的模式有效。(数据不能满足给定的子模式。)
所有这些关键字都必须设置为一个数组,其中每个项目都是一个模式。
allOf
javascript
const Ajv = require("ajv");
const ajv = new Ajv();
const schema = {
// allOf 必须满足所有子规则,即: 数组中的每一项规则
allOf: [
{ type: "string" }, // 规则1: 类型为 string
{ maxLength: 5 } // 规则2: 最大长度为5
]
}
/**
* 校验数据
* @param data {any} JSON数据
*/
function validateFn(data){
const valid = validate(data);
if (!valid) console.log(validate.errors);
return valid;
}
validateFn("short"); // 校验通过
validateFn("too long"); // 校验不通过,字符串超度超过限制(5)
anyOf
javascript
const Ajv = require("ajv");
const ajv = new Ajv();
const schema = {
// anyOf 满足其中一个子规则,即: 数组中的任一一项规则
anyOf: [
{ type: "string", maxLength: 5 }, // 规则1: 类型为 string,并且最大长度为5
{ type: "number", minimum: 0 } // 规则2: 类型为number,并且最小值为0
]
}
/**
* 校验数据
* @param data {any} JSON数据
*/
function validateFn(data){
const valid = validate(data);
if (!valid) console.log(validate.errors);
return valid;
}
validateFn("short"); // 校验通过,满足规则1
validateFn("too long"); // 校验不通过,满足规则1的类型,但是不满足规则1的长度限制
validateFn(12); // 校验通过,满足规则2
validateFn(-1); // 校验不通过,满足规则2的类型,但是不满足规则2的最小值限制
not
javascript
const Ajv = require("ajv");
const ajv = new Ajv();
const schema = {
// not 不满足子规则就通过
not: {
type: "string" // 只要不是类型为 string 的就通过
}
}
/**
* 校验数据
* @param data {any} JSON数据
*/
function validateFn(data){
const valid = validate(data);
if (!valid) console.log(validate.errors);
return valid;
}
validateFn(42); // 校验通过,类型为 number
validateFn({ "key": "value" }); //校验通过,类型为 object
validateFn("lalala"); // 校验不通过,类型为 string 了,不满足 not 条件
条件应用
新的 Draft7 中 if
, then
, else
关键字允许基于另一种模式的结果来应用子模式,这很像传统编程语言中的 if
/then
/else
构造。
- 如果
if
有效,then
也必须有效(并被else
忽略)。如果if
无效,else
也必须有效(并被then
忽略)。 - 如果
then
或else
未定义,则if
表现为它们的值为true
。 - 如果
then
和/或else
出现在没有if
,then
和 的模式中,else
则被忽略。
我们可以把它放在真值表的形式中,显示 when if
, then
, and else
are valid 的组合 以及整个模式的结果有效性:
if | then | else | whole schema |
---|---|---|---|
T | T | n/a | T |
T | F | n/a | F |
F | n/a | T | T |
F | n/a | F | F |
n/a | n/a | n/a | T |
deepRequird
ajv-keywords
库中的 deepRequird 关键字允许检查某些深层属性(由JSON指针 标识)是否可用。
- 此关键字仅适用于对象(object)。如果数据不是对象,则验证成功。
- 该值应该是一个指向数据的 JSON 指针数组,从数据中的当前位置开始。为了使数据对象有效,每个 JSON 指针都应该是数据的某个现有部分。
javascript
const Ajv = require("ajv");
const ajv = new Ajv();
// 添加 deepRequired 关键字
require('ajv-keywords')(ajv, ['deepRequired']);
// 定义 schema
const schema = {
type: "object",
// 使用 deepRequired 关键字
/**
* deepRequired 的值为 JSON指针 地址
* 当前对象下 要存在 users 这个字段
* users 类型可以为 array 或者 object
* 为 array 时,则数组下标1里面必须存在 role 字段
* 为 object 时,则 对象中 必须存在 key 为 1 的对象,并且对象中要存在 role 字段
*/
deepRequired: ["/users/1/role"]
}
// 定义json数据
const data1 = {
users: [
{},
{
id: 123,
role: "admin"
}
]
}
const data2 = {
users: [
{},
{
id: 123
}
]
}
const data3 = {
users: {
1: {
id: 123,
role: "admin"
}
}
}
const data4 = {
users: {
1: {
id: 123
}
}
}
/**
* 校验数据
* @param data {any} JSON数据
*/
function validateFn(data){
const valid = validate(data);
if (!valid) console.log(validate.errors);
return valid;
}
validateFn(data1); // 校验通过
validateFn(data2); // 校验不通过,缺少 role 字段
validateFn(data3); // 校验通过
validateFn(data4); // 校验不通过,缺少 role 字段
三、功能实现
共存
假设,我们有以下 JSON Schema 数据
javascript
const Ajv = require("ajv");
const ajv = new Ajv();
// 定义 schema
const schema = {
type: "object",
properties: {
str1: {
type: "string"
},
str2: {
type: "string"
},
obj1: {
type: "object",
properties: {
str1: {
type: "string"
},
str2: {
type: "string"
}
}
}
}
};
// 生成校验
const validate = ajv.compile(schema);
// 定义校验数据
const data1 = {
str1: 'str1',
str2: 'str2'
};
/**
* 校验数据
* @param data {any} JSON数据
*/
function validateFn(data){
const valid = validate(data);
if (!valid) console.log(validate.errors);
return valid;
}
validateFn(data1); // 校验通过,此时schema没有任何限制,传 {} 都行
添加共存
我们现在需要数据中的 str1
和 obj1.str1
共存,即:str1
和obj1.str1
必须同时存在。
javascript
const Ajv = require("ajv");
const ajv = new Ajv();
// 添加 deepRequired 关键字
require('ajv-keywords')(ajv, ['deepRequired']);
// 定义 schema
const schema = {
type: "object",
properties: {
str1: {
type: "string"
},
str2: {
type: "string"
},
obj1: {
type: "object",
properties: {
str1: {
type: "string"
},
str2: {
type: "string"
}
}
}
},
// 以下是实现
allOf: [
{
if: {
anyOf: [
{ deepRequired: ["/str1"] },
{ deepRequired: ["/obj1/str1"] }
]
},
then: {
deepRequired: ["/str1", "/obj1/str1"]
}
}
]
};
// 生成校验
const validate = ajv.compile(schema);
// 定义校验数据
const data1 = {
str1: 'str1',
str2: 'str2'
};
const data2 = {
str1: 'str1',
str2: 'str2',
obj1: {
str1: 'obj1的str1'
}
};
/**
* 校验数据
* @param data {any} JSON数据
*/
function validateFn(data){
const valid = validate(data);
if (!valid) console.log(validate.errors);
return valid;
}
validateFn(data1); // 校验不通过,缺少obj1.str1
validateFn(data2); // 校验通过
解释说明
- 我们在顶层添加了
allOf
关键,然后在里面写了一个规则; - 这个规则是一个判断条件
if
,然后在判断里面写了一个anyOf
; anyOf
中写了2个规则,deepRequired
;- 和
if
同层级写了满足条件时执行的when
; when
里面写的也是deepRequired
规则;
这其中就是通过 关键字 的组合使用来实现。deepRequired
提供深层查找字段是否存在的功能。
互斥
实现了共存之后,实现互斥只需要改动一个点,就是 when
里面,增加一个关键字 not
,让字段不能同时存在。
javascript
const Ajv = require("ajv");
const ajv = new Ajv();
// 添加 deepRequired 关键字
require('ajv-keywords')(ajv, ['deepRequired']);
// 定义 schema
const schema = {
type: "object",
properties: {
str1: {
type: "string"
},
str2: {
type: "string"
},
obj1: {
type: "object",
properties: {
str1: {
type: "string"
},
str2: {
type: "string"
}
}
}
},
allOf: [
{
if: {
anyOf: [
{ deepRequired: ["/str1"] },
{ deepRequired: ["/obj1/str1"] }
]
},
then: {
// 改动点
not: {
deepRequired: ["/str1", "/obj1/str1"]
}
}
}
]
};
// 生成校验
const validate = ajv.compile(schema);
// 定义校验数据
const data1 = {
str1: 'str1',
str2: 'str2'
};
const data2 = {
str1: 'str1',
str2: 'str2',
obj1: {
str1: 'obj1的str1'
}
};
/**
* 校验数据
* @param data {any} JSON数据
*/
function validateFn(data){
const valid = validate(data);
if (!valid) console.log(validate.errors);
return valid;
}
validateFn(data1); // 校验通过
validateFn(data2); // 校验不通过,str1 和 obj1.str1 同时存在了。
整个过程和 共存
的原理一样,只是利用 not
关键字进行取反的操作即可实现。
对于多个互斥、共存规则的处理
我们单个互斥、共存规则是在 allOf
中添加了一条规则,多个的话,接着添加就可以了。
javascript
const Ajv = require("ajv");
const ajv = new Ajv();
// 添加 deepRequired 关键字
require('ajv-keywords')(ajv, ['deepRequired']);
// 定义 schema
const schema = {
type: "object",
properties: {
str1: {
type: "string"
},
str2: {
type: "string"
},
obj1: {
type: "object",
properties: {
str1: {
type: "string"
},
str2: {
type: "string"
}
}
}
},
allOf: [
// 共存规则1
{
if: {
anyOf: [
{ deepRequired: ["/str1"] },
{ deepRequired: ["/obj1/str1"] }
]
},
then: {
deepRequired: ["/str1", "/obj1/str1"]
}
},
// 共存规则2
{
if: {
anyOf: [
{ deepRequired: ["/str2"] },
{ deepRequired: ["/obj1/str2"] }
]
},
then: {
deepRequired: ["/str2", "/obj1/str2"]
}
}
// TODO 互斥同理
]
};
// 生成校验
const validate = ajv.compile(schema);
// 定义校验数据
const data1 = {
str1: 'str1',
str2: 'str2'
};
const data2 = {
str1: 'str1',
str2: 'str2',
obj1: {
str1: 'obj1的str1'
}
};
/**
* 校验数据
* @param data {any} JSON数据
*/
function validateFn(data){
const valid = validate(data);
if (!valid) console.log(validate.errors);
return valid;
}
validateFn(data1); // 校验不通过,缺少obj1.str1 obj1.str2
validateFn(data2); // 校验不通过,缺少obj1.str2
结尾
实现 JSON Schema 的特殊规则,其实就是各种关键字的组合使用,其中关键字也可以自定义,达到完全自己掌控(deepRequired
就是一个自定义关键字哦~)。
在使用 deepRequired
时需要注意:
- 必须以
/
开头 - 必须是对象
- 只能从当前层级往下查找(包括当前层级)
校验对象下对象数组
比如数据是以下格式:
javascript
const Ajv = require("ajv");
const ajv = new Ajv();
// 添加 deepRequired 关键字
require('ajv-keywords')(ajv, ['deepRequired']);
// 定义 schema
const schema = {
type: "object",
properties: {
arr1: {
type: "array",
items: {
type: "object",
properties: {
str1: {
type: "string"
},
str2: {
type: "string"
}
}
}
},
str1: {
type: "string"
}
},
allOf: [
// 共存 str1 和 arr1[0].str1
{
if: {
anyOf: [
{ deepRequired: ["/str1"] },
{ deepRequired: ["/arr1/0/str1"] }
]
},
then: {
deepRequired: ["/str1", "/arr1/0/str1"]
}
}
]
};
// 生成校验
const validate = ajv.compile(schema);
// 定义校验数据
const data1 = {
str1: 'str1'
};
const data2 = {
str1: 'str1',
arr1: [
{
str1: 'obj1的str1'
}
]
};
/**
* 校验数据
* @param data {any} JSON数据
*/
function validateFn(data){
const valid = validate(data);
if (!valid) console.log(validate.errors);
return valid;
}
validateFn(data1); // 校验不通过,缺少arr1[0].str1
validateFn(data2); // 校验通过
可以看到,对于数组我们是根据数组的下标去查找的字段是否存在,在实际开发中,我们不可能穷举完数组的长度,所以这是不可行。 我们只能校验对象数组中的规则,如:
javascript
const Ajv = require("ajv");
const ajv = new Ajv();
// 添加 deepRequired 关键字
require('ajv-keywords')(ajv, ['deepRequired']);
// 定义 schema
const schema = {
type: "object",
properties: {
arr1: {
type: "array",
items: {
type: "object",
properties: {
str1: {
type: "string"
},
str2: {
type: "string"
}
},
allOf: [
// 对象数组中的key互相设置并校验
{
if: {
anyOf: [
{ deepRequired: ["/str1"] },
{ deepRequired: ["/str2"] }
]
},
then: {
deepRequired: ["/str1", "/str2"]
}
}
]
}
},
str1: {
type: "string"
}
}
};
// 生成校验
const validate = ajv.compile(schema);
// 定义校验数据
const data1 = {
str1: 'str1'
};
const data2 = {
str1: 'str1',
arr1: [
{
str1: 'obj1的str1'
}
]
};
/**
* 校验数据
* @param data {any} JSON数据
*/
function validateFn(data){
const valid = validate(data);
if (!valid) console.log(validate.errors);
return valid;
}
validateFn(data1); // 校验通过
validateFn(data2); // 校验不通过,数组对象中缺少str2
对于实现感觉也可以使用 implication
implication
实现,应该还会更加简化一些,但是没有时间去研究啦,各位读者大大可以尝试去实现一下~