如何在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

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

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

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 指针都应该是数据的某个现有部分。
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 数据

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必须同时存在。

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规则;

图片[1]-如何在JSON Schema中实现互斥、共存-五八三
这其中就是通过 关键字 的组合使用来实现。deepRequired提供深层查找字段是否存在的功能。

互斥

实现了共存之后,实现互斥只需要改动一个点,就是 when里面,增加一个关键字 not,让字段不能同时存在。

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中添加了一条规则,多个的话,接着添加就可以了。

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时需要注意:

  • 必须以 /开头
  • 必须是对象
  • 只能从当前层级往下查找(包括当前层级)

校验对象下对象数组

比如数据是以下格式:

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); // 校验通过

可以看到,对于数组我们是根据数组的下标去查找的字段是否存在,在实际开发中,我们不可能穷举完数组的长度,所以这是不可行。
我们只能校验对象数组中的规则,如:

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实现,应该还会更加简化一些,但是没有时间去研究啦,各位读者大大可以尝试去实现一下~

参考文献

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYrXMT3u' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片