☰
express-validator 数组校验陷阱深度解析:toString 转换机制与通配符(Wildcards)解决方案
2026/10/10 9:07:50 网站建设 项目流程
  • 后端

【免费下载链接】express-validator

An express.js middleware for validator.js.

项目地址:https://gitcode.com/gh_mirrors/ex/express-validator
点击查看免费下载

express-validator 作为 Express.js 生态中最常用的校验中间件,其"标准校验器/标准清洗器"均来自 validator.js,而 validator.js 的函数只接受字符串输入。本文基于 v6.10.0 FAQ 文档 的核心问答,深入剖析"为什么数组不会被正确校验/清洗"这一经典陷阱:从toString()的类型转换源码逐行拆解,到数组只处理首元素的根因,再到用通配符*覆盖数组全部元素的实战方案。读完后你将理解 express-validator 的内部值转换链路,并能正确写出对数组逐元素生效的校验与清洗规则。

一、问题背景:标准校验器为何要先做字符串转换

在 Validation Chain API 与 Sanitization Chain API 中,express-validator 明确说明:

  • 标准校验器(Standard validators):validator.js 暴露的所有校验器,如isInt、isEmail、contains、isString等,均可直接在链上调用;
  • 标准清洗器(Standard sanitizers):validator.js 暴露的所有清洗器,如normalizeEmail、trim、toInt、escape等。

由于validator.js 只接受string作为输入,因此任何需要交给标准校验器或标准清洗器处理的值(包括数组和对象),都必须先被转换成字符串类型。这一转换发生在 express-validator 内部,由toString函数完成。

也正因如此,v6.10.0 的 FAQ 开篇就抛出了那个著名问题:"为什么数组没有被正确校验/清洗?"——答案就藏在toString的实现里。

二、toString 源码逐行拆解:数组为何只处理首元素

v6.10.0 FAQ 文档给出了当时版本中toString的完整实现:

export function toString(value: any, deep = true): string { if (Array.isArray(value) && value.length && deep) { return toString(value[0], false); } else if (value instanceof Date) { return value.toISOString(); } else if (value && typeof value === 'object' && value.toString) { if (typeof value.toString !== 'function') { return Object.getPrototypeOf(value).toString.call(value); } return value.toString(); } else if (value == null || (isNaN(value) && !value.length)) { return ''; } return String(value); }

逐条拆解这段代码,可以看到完整的转换优先级:

分支条件处理逻辑返回示例
Array.isArray(value) && value.length && deep递归调用自身,只取数组第一个元素value[0],且传入deep = false防止继续递归['sunday', 100]→ 只处理'sunday'
value instanceof Date转为 ISO 字符串new Date()→2026-10-09T...Z
对象且带有toString优先调用自定义toString();若toString不是函数则回退到原型链上的Object.prototype.toString{ toString: () => 'foo' }→'foo'
value == null或isNaN(value) && !value.length返回空字符串null/undefined/NaN→''
其余情况兜底使用原生String(value)false→'false',100→'100'

关键结论:当校验或清洗的目标是数组时,只有数组的第一个元素会被送入 validator.js 的校验器/清洗器,其余元素被完全忽略。FAQ 原文对此的表述是:

As we can see above, when validating or sanitizing anarrayonly the first element of it is processed.

从仓库源码看,utils.spec.ts 中的测试用例也印证了转换规则:false → 'false'、null → ''、undefined → ''、NaN → ''、Date 对象 → ISO 字符串、带自定义toString的对象 → 自定义返回值、普通对象 →'[object Object]'、数组[1, 2, 3]→'1,2,3'(数组整体被String()化为逗号拼接的字符串)。这些用例为理解转换边界提供了可验证的依据。

版本演进提示:需要说明的是,上述带数组分支的toString是 v6.10.0 文档记录的实现。在当前仓库源码 src/utils.ts 中,toString已经去掉了数组递归分支,数组的逐元素处理被下沉到了具体的校验/清洗 context item 中(详见第五节)。FAQ 所描述的问题形态主要针对 v6.10.0 及更早版本,但"值会先被字符串化再交给 validator.js"这一核心约束在后续版本中依然成立。

三、问题复现:isString 校验数组为何会误判通过

FAQ 给出了一个非常典型、也非常容易踩坑的例子:

// weekdays: ['sunday', 100] body('weekdays').isString(); // 通过校验(错误地) body('weekdays.*').isString(); // 不通过校验(正确地)

两个链条的差异解释如下:

  • 第一条链body('weekdays').isString():字段名直接指向整个数组。由于只处理首元素,实际被校验的值是'sunday'(字符串),isString('sunday')返回true,校验错误地通过了——即使数组里混入了数字100也毫不知情。
  • 第二条链body('weekdays.*').isString():字段名使用通配符,逐个命中数组的每个元素。元素'sunday'通过,而元素100(数字)不满足isString,校验正确地返回错误。

FAQ 原文对此的总结是:

In this example the first chain processes only the first element of the array and the validation erroneously passes. In the second one, instead, all the elements are validated and the chain correctly returns an error.

需要注意的是,"只处理首元素"影响的不仅是校验器,清洗器同样受影响。例如body('weekdays').trim()只会对首元素做trim,数组里的其他元素会原样保留;想要逐元素清洗同样需要通配符。

四、解决方案:用通配符 * 校验/清洗数组的全部值

FAQ 给出的官方建议非常明确:

You can use wildcards to validate/sanitize all the values of the array.

通配符(wildcard)用*字符表示,适用于"对数组的所有元素或对象的所有键应用相同规则"的场景。以 v6.10.0 的通配符文档 为例,假设你要校验所有地址的邮政编码合法,并把每个地址的number字段清洗为整数:

const express = require('express'); const { check } = require('express-validator'); const app = express(); app.use(express.json()); app.post( '/addresses', check('addresses.*.postalCode').isPostalCode(), check('addresses.*.number').toInt(), (req, res) => { // Handle the request }, );

这段代码可以同时正确处理两种请求体形态:

形态一:地址是数组

{ "addresses": [ { "postalCode": "2010", "number": "500" }, { "postalCode": "", "number": "501" } ] }

addresses.*.postalCode会命中数组中的每个元素(2010与''),第二个地址的邮政编码为空,isPostalCode校验失败——这正是通配符的价值:没有通配符时,只有数组的第一个地址会被校验。

形态二:地址是预定义键的对象

{ "addresses": { "home": { "postalCode": "", "number": "501" }, "work": { "postalCode": "2010", "number": "500" } } }

*同样可以匹配对象的所有键(home、work),home.postalCode为空导致校验失败。通配符对"数组元素"和"对象键"是通用的。

4.1 通配符与数组相关 API 的配合使用

在 v6.10.0 的 Validation Chain API 中,还有两处与数组强相关的细节值得注意:

  • .notEmpty()同样只校验首元素:文档明确指出,"这不是用来检查数组长度大于零的,因为.notEmpty()只会校验数组的第一个元素"。因此:
// weekdays: ['sunday', 'monday'] check('weekdays').notEmpty(); // 通过校验 // names: ['', 'John'] check('names').notEmpty(); // 不通过,因为 names[0] 为空

如果需要"数组长度至少为 1",请改用.isArray({ min: 1 })。

  • .isArray(options):接受{ min, max }选项分别限定数组的最小、最大长度,这是对数组"整体"做校验的正确方式,与通配符的"逐元素"校验是互补关系。

五、源码纵深:当前版本中数组是如何逐元素处理的

虽然 FAQ 成文于 v6.10.0,但当前仓库的源码已经对数组处理做了重构,从"只取首元素"演进为"逐元素处理"。从源码结构看,这一变化体现在两个 context item 中:

1. 标准校验器:src/context-items/standard-validation.ts

async run(context: Context, value: any, meta: Meta) { const values = Array.isArray(value) ? value : [value]; values.forEach(value => { const result = this.validator(this.stringify(value), ...this.options); if (this.negated ? result : !result) { context.addError({ type: 'field', message: this.message, value, meta }); } }); }

当前实现会把数组展开成多个值,对每个元素分别调用stringify(即toString)后再交给 validator.js 校验器,并针对每个失败元素单独记录错误。这一点在 standard-validation.spec.ts 的测试中得到了印证:对数组[1, 42],toString会被调用两次,validator 分别收到'hey1'与'hey42'。

2. 标准清洗器:src/context-items/sanitization.ts

const values = Array.isArray(value) ? value : [value]; const newValues = values.map(value => { return (this.sanitizer as StandardSanitizer)(this.stringify(value), ...this.options); }); // We get only the first value of the array if the original value was wrapped. context.setData(path, values !== value ? newValues[0] : newValues, location);

清洗器同样对数组逐元素执行清洗,并把清洗后的数组整体写回字段。

由此可以推断:在新版本中,body('weekdays').isString()这类"直接作用于整个数组"的校验,其行为与 v6.10.0 已有本质区别——数组的每个元素都会被逐一校验。但仍有两点约束始终存在:

  1. 字符串化仍发生在 validator.js 之前:无论数组还是单个值,最终进入 validator.js 的都是字符串。例如数字100会被转为'100'再交给校验器,这会带来类型上的"失真",设计校验规则时需时刻留意。
  2. 通配符仍然是最明确、最可控的逐元素方案:它让你显式声明"我要校验数组的每一项",而非依赖数组展开的隐式行为;同时它对对象键同样生效,语义更清晰。

六、实战建议与常见误区总结

把 FAQ 的结论落到工程实践,可以归纳为四条准则:

  1. 校验/清洗数组元素,优先使用通配符:body('items.*').isInt()、body('tags.*').trim()。通配符是 v6.10.0 FAQ 给出的官方推荐方案,也是语义最清晰的做法。
  2. 对数组"整体"做约束,使用专门的 API:数组长度用.isArray({ min, max }),存在性检查用.exists()/.optional(),不要指望notEmpty()或普通标准校验器替你判断数组结构。
  3. 留意类型转换失真:任何标准校验器/清洗器拿到手的一定是字符串。数字、布尔、对象在被校验前都会先经过toString的转换规则(参考 utils.spec.ts 中的用例),必要时用.custom()校验原始值类型。
  4. 结合字段选择规则设计复杂路径:通配符可与嵌套路径组合,如addresses.*.postalCode,覆盖数组与对象两种数据形态;更完整的字段选择能力可参考 字段选择指南。

最后补充 FAQ 中记录的三个关联问题编号(#791、#883、#931),它们分别对应了数组校验/清洗行为相关的历史缺陷与讨论,可作为追溯该问题演进脉络的线索。

七、小结

"数组没有被正确校验/清洗"并非 express-validator 的随机缺陷,而是其设计约束下的必然结果:validator.js 只认字符串,而 v6.10.0 的toString转换函数只取了数组首元素。理解这条转换链路(src/utils.ts 中的toString及 utils.spec.ts 的测试用例),再配合通配符*对数组逐元素命中,就能完全避开这个陷阱。对于使用新版仓库代码的读者,标准校验器/清洗器已改为逐元素处理,但"先字符串化再校验"的约束与"通配符是最显式方案"的结论依然成立,两者结合使用才能写出既正确又易读的数组校验规则。

  • 后端

【免费下载链接】express-validator

An express.js middleware for validator.js.

项目地址:https://gitcode.com/gh_mirrors/ex/express-validator
点击查看免费下载
上一篇:ParlAI Model Chat 众包任务指南:采集人类与模型对话数据并批量评估生成质量
下一篇:弹幕为什么会丝滑:bullet-screen-cj消息循环与双线程绘制架构源码剖析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询