如何编写 JSON Schema 验证?

解读

在国内 CouchDB 岗位面试中,面试官问“如何编写 JSON Schema 验证”并不是想听你背一段 JSON 语法,而是考察三件事:

  1. 你是否知道 CouchDB 本身不强制存储 Schema,但可以通过 validate_doc_update 函数在设计文档里实现服务器端准入校验
  2. 你是否能把 JSON Schema(Draft-07 最常用)翻译成 JavaScript 代码并塞进 design doc;
  3. 你是否理解这种校验对移动端离线同步多主复制冲突数据质量治理带来的实际价值。
    答不到这三层,基本会被判定为“只用过 CouchDB 当 MongoDB 替代品”。

知识点

  1. validate_doc_update 函数签名
    function(newDoc, oldDoc, userCtx, secObj)
    抛出异常即拒绝写入,无异常即通过。
  2. JSON Schema 核心关键字:type、required、properties、additionalProperties、pattern、minimum/maximum、minLength/maxLength、enum。
  3. CouchDB 内建对象
    • userCtx.roles → 当前用户角色数组
    • secObj.admins → 数据库级管理员配置
      校验时经常把“字段规则”与“角色规则”混写在一起。
  4. 国内合规点
    • 敏感字段(如手机号、身份证)必须 pattern 匹配国标正则;
    • 日志审计要求把拒绝原因写入 CouchDB log(可通过 throw 字符串实现)。
  5. 性能陷阱
    • 避免在 validate_doc_update 里做 HTTP 外部调用
    • 复杂正则先在本地单元测试,防止 ReDoS 拖垮写入吞吐。

答案

下面给出可直接粘贴到设计文档里的完整示例,演示如何把 JSON Schema 映射成 validate_doc_update 函数。场景:订单文档,只允许 finance 角色把 status 从 pending 改成 paid,且金额必须 ≥0.01、手机号必须是中国大陆 11 位。

{ "_id": "_design/order", "validate_doc_update": " function (newDoc, oldDoc, userCtx) { // 1. 只校验 type=order 的文档 if (newDoc.type !== 'order') return;

  // 2. 定义 JSON Schema 规则
  var schema = {
    required: ['amount', 'mobile', 'status'],
    properties: {
      amount:    { type: 'number', minimum: 0.01 },
      mobile:    { type: 'string', pattern: '^1[3-9]\\\\d{9}$' },
      status:    { type: 'string', enum: ['pending', 'paid', 'cancelled'] }
    },
    additionalProperties: false
  };

  // 3. 通用校验器
  function validate(obj, s) {
    if (s.required) {
      s.required.forEach(function(k) {
        if (obj[k] === undefined)
          throw 'required field missing: ' + k;
      });
    }
    if (s.additionalProperties === false) {
      Object.keys(obj).forEach(function(k) {
        if (k.charAt(0) !== '_' && !s.properties[k])
          throw 'additional field not allowed: ' + k;
      });
    }
    Object.keys(s.properties || {}).forEach(function(k) {
      var rule = s.properties[k];
      var val  = obj[k];
      if (val === undefined) return;
      if (rule.type && typeof val !== rule.type)
        throw k + ' must be ' + rule.type;
      if (rule.minimum !== undefined && val < rule.minimum)
        throw k + ' below minimum ' + rule.minimum;
      if (rule.pattern && !new RegExp(rule.pattern).test(val))
        throw k + ' format error';
      if (rule.enum && rule.enum.indexOf(val) === -1)
        throw k + ' must be one of ' + JSON.stringify(rule.enum);
    });
  }

  // 4. 执行校验
  validate(newDoc, schema);

  // 5. 业务级权限:只有 finance 角色可把状态改为 paid
  if (oldDoc && oldDoc.status === 'pending' && newDoc.status === 'paid') {
    if (!userCtx.roles || userCtx.roles.indexOf('finance') === -1)
      throw 'only finance can approve payment';
  }
}

" }

把该设计文档 PUT 到目标数据库后,所有订单写入都会走上述 JSON Schema 校验,拒绝不符合规则的文档并返回 403 forbidden,同时在 CouchDB 日志里留下具体错误信息,方便运维审计。

拓展思考

  1. 动态 Schema 演进:国内业务迭代快,可在设计文档里加 "schemaVersion" 字段,通过 switch(version) 实现多版本共存,避免停机迁移。
  2. 与 OpenAPI 联动:后端团队通常先用 Swagger/OpenAPI 3.0 定义接口,再把 JSON Schema 自动翻译成 validate_doc_update,降低“文档与库不一致”风险。
  3. 离线场景冲突:移动端可能先离线写入,再同步到云端。若云端 Schema 升级,需保证 新 Schema 向前兼容或使用 filtered replication 把旧客户端路由到旧版本数据库,防止同步失败。
  4. 性能压测:在 4 核 8 G 的国产 ARM 服务器上,单个 validate_doc_update 函数若包含 20 条正则,写入 QPS 会从 6 k 降到 3 k;建议把 静态正则提前编译 并缓存到函数闭包内,减少重复实例化开销。