如何编写 JSON Schema 验证?
解读
在国内 CouchDB 岗位面试中,面试官问“如何编写 JSON Schema 验证”并不是想听你背一段 JSON 语法,而是考察三件事:
- 你是否知道 CouchDB 本身不强制存储 Schema,但可以通过 validate_doc_update 函数在设计文档里实现服务器端准入校验;
- 你是否能把 JSON Schema(Draft-07 最常用)翻译成 JavaScript 代码并塞进 design doc;
- 你是否理解这种校验对移动端离线同步、多主复制冲突、数据质量治理带来的实际价值。
答不到这三层,基本会被判定为“只用过 CouchDB 当 MongoDB 替代品”。
知识点
- validate_doc_update 函数签名:
function(newDoc, oldDoc, userCtx, secObj)
抛出异常即拒绝写入,无异常即通过。 - JSON Schema 核心关键字:type、required、properties、additionalProperties、pattern、minimum/maximum、minLength/maxLength、enum。
- CouchDB 内建对象:
- userCtx.roles → 当前用户角色数组
- secObj.admins → 数据库级管理员配置
校验时经常把“字段规则”与“角色规则”混写在一起。
- 国内合规点:
- 敏感字段(如手机号、身份证)必须 pattern 匹配国标正则;
- 日志审计要求把拒绝原因写入 CouchDB log(可通过 throw 字符串实现)。
- 性能陷阱:
- 避免在 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 日志里留下具体错误信息,方便运维审计。
拓展思考
- 动态 Schema 演进:国内业务迭代快,可在设计文档里加 "schemaVersion" 字段,通过 switch(version) 实现多版本共存,避免停机迁移。
- 与 OpenAPI 联动:后端团队通常先用 Swagger/OpenAPI 3.0 定义接口,再把 JSON Schema 自动翻译成 validate_doc_update,降低“文档与库不一致”风险。
- 离线场景冲突:移动端可能先离线写入,再同步到云端。若云端 Schema 升级,需保证 新 Schema 向前兼容或使用 filtered replication 把旧客户端路由到旧版本数据库,防止同步失败。
- 性能压测:在 4 核 8 G 的国产 ARM 服务器上,单个 validate_doc_update 函数若包含 20 条正则,写入 QPS 会从 6 k 降到 3 k;建议把 静态正则提前编译 并缓存到函数闭包内,减少重复实例化开销。