Egg: [RFC] egg-validate 支持标准 jsonSchema 的校验

Created on 28 Jul 2017  ·  9Comments  ·  Source: eggjs/egg

背景

目前 egg-validate 底层是 parameter,有固定的 rule 格式,但并不是标准的 jsonSchema 校验,某些校验场景比较复杂的情况,parameter 的能力无法满足

案例

要校验的 json

[{
  "name": "1",
  "status": "success"
}, {
  "name": "2",
  "status": "failed",
  "failedMessage": "reason"
}]

校验需求

status=failed 时,必须有 failedMessage 这个字段,status=success,必然没有 failedMessage 这个字段

这时基于 parameter 的特性就无法直接通过定义 rule 来满足,而是要组合程序逻辑去分别 validate

但是通过 jsonSchema 就能直接定义出来

{
  "type": "array",
  "item": {
    "oneOf": [{
      "type": "object",
      "properties" : {
         "name": {
            "type": "string"
         },
          "status": {
            "type": "string"
         }
      },
      "required": ["name", "status"]
    }, {
      "type": "object",
      "properties" : {
         "name": {
            "type": "string"
         },
          "status": {
            "type": "string"
         },
         "failedMessage": {
            "type": "string"
         }
      },
      "required": ["name", "status", "failedMessage"]
    }]
  }
}

方案

引入 ajv,目前社区最好的 jsonSchema validate 库。http://epoberezkin.github.io/ajv/

egg-validate 插件,ctx 注入额外的方法

// app/extend/context.js
'use strict';

module.exports = {
  /**
   * validate data with rules
   *
   * @param  {Object} rules  - validate rule object, see [parameter](https://github.com/node-modules/parameter)
   * @param  {Object} [data] - validate target, default to `this.request.body`
   */
  validate(rules, data) {
    data = data || this.request.body;
    const errors = this.app.validator.validate(rules, data);
    if (errors) {
      this.throw(422, 'Validation Failed', {
        code: 'invalid_param',
        errors,
      });
    }
  },
+ jsonSchemaValidate(jsonSchema, data) {
+    data = data || this.request.body;
+    const errors = this.app.jsonSchemaValidator.validate(jsonSchema, data);
+    if (errors) {
+      this.throw(422, 'Validation Failed', {
+        code: 'invalid_param',
+        errors,
+      });
+    }
+ },
};
// app.js
'use strict';

const Parameter = require('parameter');
+ const Ajv = require('ajv');

module.exports = app => {
  /**
   * Validate
   *
   * ```js
   * app.validator.addRule('jsonString', (rule, value) => {
   *   try {
   *     JSON.parse(value);
   *   } catch (err) {
   *     return 'must be json string';
   *   }
   * });
   *
   * app.validator.validate({
   *     name: 'string',
   *     info: { type: 'jsonString', required: false },
   * }, {
   *   name: 'Egg',
   *   info: '{"foo": "bar"}',
   * });
   * ```
   */
  app.validator = new Parameter();

+ app.jsonSchemaValidator = new Ajv();
};

需要讨论的点

是否可以使用 parameteraddRule 来扩展?

这个思路是 @atian25 提出的,我想了一下,场景可能不太合适。

addRule 更多是针对整个 json 的某个字段的 rule 扩展,当然通过 addRule 它也可以实现对根的 json 进行全局的校验,但是这样使用起来会很怪

用标准 jsonSchema 进行校验成本有点高

毕竟 jsonSchema 不是所有人都熟悉,而且 jsonSchema 的代码量比描述的 json 其实要长很多。这样特性使用起来可能成本有点高

所以方案的设计也是额外扩展的,在必要时才会去使用,绝大部分情况其实用 parameter 就能解决了

egg-validate proposals

Most helpful comment

我建议独立插件好了,egg-validate-schema,API 是 validateBySchema

cc @eggjs/core 投票下吧,好让 @mansonchor 开工~

直接在本 comment 用 👍 👎 投?

  • 👍 独立插件
  • 👎 直接做到 egg-validate

All 9 comments

这么复杂的检验规则还不如写代码,还得测试这个规则是否被测试过。

新增 jsonSchemaValidate 接口没问题啊

放到 egg-validate 里面么?还是单独实现一个 egg-ajv 插件好了

@popomore 我现在就是写代码分步校验的,上面只是相对简单的情况,目前我遇到的场景比描述的复杂很多,json 结构非常复杂而且包含许多 存在这个字段,才会有另外某些字段 的操蛋逻辑

代码可能就会变成这样

const requestBody = ctx.request.body;
// 校验最外层结构
ctx.validate(rule, requestBody);

if (requestBody.xxx === 1) {
  // 对应情况 1 的校验
  ctx.validate(rule1, requestBody.xxxData);
} else if (requestBody.xxx === 2) {
  // 对应情况 2 的校验
  ctx.validate(rule2, requestBody.xxxData);
} else {
  // 可能还会有第三层的分支逻辑...
}

这样就会变得非常蛋疼,虽然 jsonSchema 相对复杂,但它是标准的,通过 ajv 是能保证 jsonSchema 正确性的,从而也能通过正确的 jsonSchema 去校验真实的复杂数据源

相对写分支逻辑,虽然复杂但还是 jsonSchema 更优雅点


另外目前的 parameter 不支持对一个字段定义多个类型也是个痛点,而这是 jsonSchema 灵活的地方

{
    "type": "object",
    "properties": {
        "name": {
            "type": [
                "string",
                "number"
            ]
        }
    }
}

@dead-horse 我觉得可以放 egg-validate,因为本质还是进行输入校验,只是校验规则的定义不一样

我的提案是向下兼容的,不影响旧有 ctx.validate 的使用,只是多注入一个接口

接口名 validateJSON 吧。

独立一个 egg-validate-json 也行。

还有一种方案是 addRule(json) 加个 type,这样既可以用 scheme 单独验证某个字段,需要全部验证的时候就 ctx.validate(rule, { body: requestBody } )

@atian25 addRule 不考虑了,根本是两种不一样的校验规则定义,还是区分开好

名字建议是 validateBySchema,我觉得和 json 不搭,旧有的 validate 校验的也是 json


现在基本要明确的就是基于 egg-validate 增加接口还是独立一个插件

我建议独立插件好了,egg-validate-schema,API 是 validateBySchema

cc @eggjs/core 投票下吧,好让 @mansonchor 开工~

直接在本 comment 用 👍 👎 投?

  • 👍 独立插件
  • 👎 直接做到 egg-validate

@eggjs/core @atian25 只有两票...

如果都没意见我就开工咯,预计这周内抽时间完成

Was this page helpful?
0 / 5 - 0 ratings

Related issues

xcstream picture xcstream  ·  3Comments

yuu2lee4 picture yuu2lee4  ·  3Comments

Azard picture Azard  ·  3Comments

Leungkingman picture Leungkingman  ·  3Comments

Webjiacheng picture Webjiacheng  ·  3Comments