如何在JSON Schema中实现互斥、共存

原创 鲜正权 / 叫叫技术团队

前言

这次需求中,需要对JSON数据里面的字段进行判断(互斥、共存),比如:数据中有2个字段 a、b,那么互斥就是:有a就不能有b,有b就不能有a,它俩只能存在一个。共存:a、b必须同时存在,有a就必须有b,有b就必须有a。那么,我们能实现这个需求呢?答案是:能!

一、什么是JSON-Schema

JSON Schema 是用于验证 JSON 数据结构的强大工具,Schema可以理解为模式或者规则。

我们这边只做简要的介绍,详情请参阅:

二、前置条件

要实现这次的需求,我们需要用到 JSON Schema 的 组合条件应用 以及 ajv-keywords 库。 本文需要对 JSON Schema 有一定的了解。 本文中所有校验都是使用的 ajv

组合

我们需要使用到组合里面的 allOfanyOfnot。详情参阅: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忽略)。
  • 如果thenelse未定义,则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没有任何限制,传 {} 都行

添加共存

我们现在需要数据中的 str1obj1.str1 共存,即:str1obj1.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); // 校验通过

解释说明

  1. 我们在顶层添加了 allOf关键,然后在里面写了一个规则;
  2. 这个规则是一个判断条件 if,然后在判断里面写了一个 anyOf;
  3. anyOf中写了2个规则,deepRequired;
  4. if同层级写了满足条件时执行的 when;
  5. 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实现,应该还会更加简化一些,但是没有时间去研究啦,各位读者大大可以尝试去实现一下~

参考文献

相关推荐
Lenyiin28 分钟前
02.06、回文链表
数据结构·leetcode·链表
爪哇学长31 分钟前
双指针算法详解:原理、应用场景及代码示例
java·数据结构·算法
爱摸鱼的孔乙己33 分钟前
【数据结构】链表(leetcode)
c语言·数据结构·c++·链表·csdn
烦躁的大鼻嘎1 小时前
模拟算法实例讲解:从理论到实践的编程之旅
数据结构·c++·算法·leetcode
C++忠实粉丝1 小时前
计算机网络socket编程(4)_TCP socket API 详解
网络·数据结构·c++·网络协议·tcp/ip·计算机网络·算法
daiyang123...3 小时前
测试岗位应该学什么
数据结构
kitesxian3 小时前
Leetcode448. 找到所有数组中消失的数字(HOT100)+Leetcode139. 单词拆分(HOT100)
数据结构·算法·leetcode
薯条不要番茄酱5 小时前
数据结构-8.Java. 七大排序算法(中篇)
java·开发语言·数据结构·后端·算法·排序算法·intellij-idea
盼海7 小时前
排序算法(五)--归并排序
数据结构·算法·排序算法
搬砖的小码农_Sky13 小时前
C语言:数组
c语言·数据结构