news 2026/10/10 2:02:53

基于 `checkSchema()` 的声明式请求校验:express-validator 模式化验证完全指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于 `checkSchema()` 的声明式请求校验:express-validator 模式化验证完全指南
  • 后端

【免费下载链接】express-validator

An express.js middleware for validator.js.

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

导读

checkSchema()是 express-validator 提供的一种声明式校验 API:它允许你用一个纯对象(schema)一次性描述多个字段的校验、清洗规则,并自动为每个字段生成对应的校验链(ValidationChain),直接作为中间件挂载到 express.js 路由上。本文以官方文档 docs/api/check-schema.md 为骨架,结合 src/middlewares/schema.ts 的源码实现与 src/middlewares/schema.spec.ts 的测试用例,系统讲解 Schema 的全部核心要素——内置校验器/清洗器的配置方式、options/bail/if/negated/errorMessage等修饰符、字段级修饰符(in/optional)、自定义校验器与清洗器的两种写法,以及手动运行checkSchema()的实战方案。


checkSchema()是什么

函数签名

checkSchema(schema: Schema, defaultLocations?: Location[]): ValidationChain[] & ContextRunner

checkSchema()会根据传入的schema生成一个校验链列表(每个字段对应一条ValidationChain),这个列表既可以作为 express.js 路由中间件使用,又因为同时实现了ContextRunner接口而可以被手动运行。

源码中,checkSchema是通过createCheckSchema(check)创建的(见 src/middlewares/schema.ts#L286),其返回类型为RunnableValidationChains<C>,即“校验链数组 + 一个run(req)方法”的组合体(见 src/middlewares/schema.ts#L143-L145):

export type RunnableValidationChains<C extends ValidationChainLike> = C[] & { run(req: Request): Promise<ResultWithContext[]>; };

基础用法:注册到 express 路由

最典型的使用方式是把checkSchema()的结果直接作为中间件传入路由:

app.post( '/signup', checkSchema({ email: { isEmail: true }, password: { isLength: { options: { min: 8 } } }, }), (req, res) => { // Handle request }, );

默认的校验位置(request locations)

默认情况下,schema 中所有字段都会在全部请求位置中校验,即body、cookies、headers、params和query五处都会检查。

这一点在源码中有直接体现:checkSchema的defaultLocations参数默认值为validLocations(见 src/middlewares/schema.ts#L147 与 src/middlewares/schema.ts#L211):

const validLocations: Location[] = ['body', 'cookies', 'headers', 'params', 'query'];

对应的测试用例也验证了这一点(见 src/middlewares/schema.spec.ts#L28-L40):空 schema 生成的 chain 其locations正是上述五个位置。

如果你只想在部分位置校验,可以通过第二个参数指定。例如只在body和query中校验:

checkSchema(schema, ['body', 'query']);

如果需要按字段精细化控制校验位置,则使用字段级的in属性,它优先于defaultLocations参数。源码中ensureLocations()函数(见 src/middlewares/schema.ts#L288-L296)的逻辑是:如果config.in存在(字符串或数组),则使用它;否则回退到defaultLocations,最后再过滤掉非法位置。


手动运行checkSchema()

checkSchema()返回的本质上是一个中间件,直接传给 express 路由最理想。但由于它也实现了ContextRunner接口,你也可以在自定义的中间件或路由处理器中手动运行它:

app.post('/signup', async (req, res) => { const results = await checkSchema({ email: { isEmail: true }, password: { isLength: { options: { min: 8 } } }, }).run(req); const hasErrors = results.some(result => !result.isEmpty()); if (hasErrors) { const errors = results.flatMap(result => result.array()); return res.status(400).json({ errors }); } });

几点值得注意的细节:

  • 每个字段对应一个独立的结果(Result),所以要用results.some(...)判断是否有任何字段出错,用results.flatMap(result => result.array())把所有错误扁平化聚合成一个数组。
  • 源码中run方法被实现为runAllChains(req, chains)(见 src/middlewares/schema.ts#L272),即并行运行所有校验链并返回各自的结果数组。
  • 手动运行相关更完整的方案(如“顺序执行、遇到第一个失败即停止”的通用validate()包装器、以及用.if()做条件校验)可参考 docs/guides/manually-running.md。
  • 另外,ContextRunner的run(req, options?)支持{ dryRun: boolean }选项:默认会把校验/清洗结果回写到req(影响validationResult(req)与已被清洗字段的取值),设置dryRun: true则只运行校验并返回结果而不回写(详见 docs/api/misc.md#contextrunner)。

:::tip

更多手动运行校验链的细节,参见指南 docs/guides/manually-running.md。

:::


Schema 的结构与类型体系

Schema本质是一个从字段路径到字段 schema 的映射对象。字段路径(field path)决定了选择请求中的哪些字段,字段 schema 决定了这些字段如何被校验与清洗。

字段路径的语法(如addresses.work.country、siblings[0].name、websites["www.example.com"],以及通配符*与 globstar**)参见 docs/guides/field-selection.md。

一个字段 schema 的键(key)可以是以下四类中的一种或多种组合:

  1. 内置校验器(built-in validators)
  2. 内置清洗器(built-in sanitizers)
  3. 字段修饰符(field modifiers)
  4. 其他任意名称 —— 表示自定义校验器或自定义清洗器

如果键不属于以上任何一类,则它必须是一个自定义 schema(即以任意名称包裹custom/customSanitizer的写法)。

从源码看,TypeScript 类型体系如下(见 src/middlewares/schema.ts#L92-L138):

  • BaseParamSchema:字段级通用配置,包括in、errorMessage、optional;
  • ValidatorsSchema/SanitizersSchema:内置校验器与清洗器的配置(每个键的值可以是boolean或带options等配置的对象);
  • ParamSchema<T>:三者交叉,并允许任意扩展键T(当键不在内置范围内时,其值为CustomValidatorSchemaOptions或CustomSanitizerSchemaOptions);
  • Schema<T> = Record<string, ParamSchema<T>>:字段名到字段 schema 的映射。

checkSchema()的返回链还通过RunnableValidationChains类型标注了“数组 +run”的组合形态。


内置校验器(Built-in Validators)

任何ValidationChain上的内置校验器(如isEmail、isLength、isEmpty、isInt、notEmpty、matches等)都可以直接作为字段 schema 的键使用。注意not与withMessage两个方法被源码明确排除在 schema 键之外(见 src/middlewares/schema.ts#L169 与测试 src/middlewares/schema.spec.ts#L118-L133),因为它们分别对应 schema 中的negated与errorMessage修饰符。

值为true:无参数开启

如果内置校验器被设置为true,表示无参数开启该校验:

checkSchema({ email: { isEmail: true }, password: { notEmpty: true }, });
check('email').isEmail(); check('password').notEmpty();

值为对象:带配置开启

校验器的值也可以是一个对象,此时校验器将携带额外配置开启,可配置的属性包括options、bail、if、negated、errorMessage。这些配置对应的源码类型定义在BaseValidatorSchemaOptions(见 src/middlewares/schema.ts#L24-L46),且if、negated会在校验器之前插入链中,bail、errorMessage则在校验器之后追加(见 src/middlewares/schema.ts#L242-L266)。

options

设置校验器的参数。当有多个参数时,options必须是数组;只有一个参数时可以直接传值。

checkSchema({ phone: { isMobilePhone: { options: ['any', { strictMode: true }], }, }, password: { isLength: { options: { min: 8 }, }, }, });
check('phone').isMobilePhone('any', { strictMode: true, }); check('password').isLength({ min: 8 });

特别提醒(数组参数):如果传给校验器的唯一参数本身是一个数组,那么它必须再被包裹一层数组。典型场景是isIn:

checkSchema({ weekend: { // 👎 会翻译成 `isIn('saturday', 'sunday')` —— 错误 isIn: { options: ['saturday', 'sunday'] }, // 👍 会翻译成 `isIn(['saturday', 'sunday'])` —— 正确 isIn: { options: [['saturday', 'sunday']] }, }, });

源码中这一逻辑由_.castArray(entry[1].options)实现(见 src/middlewares/schema.ts#L250):options总会先被规范成数组,再作为展开参数传给校验器,因此options: [['saturday', 'sunday']]最终展开为isIn(['saturday', 'sunday'])。

bail

如果当前校验器(或之前任意校验器)失败,则停止继续运行后续校验链。等价于在链上使用.bail()。

checkSchema({ email: { // 先运行 isEmail;如果 email 不合法,则下面的自定义校验器 checkEmailNotInUse 不会运行 isEmail: { bail: true }, custom: { options: checkEmailNotInUse }, }, });

等价于:

check('email').isEmail().bail().custom(checkEmailNotInUse);

源码在链上调用chain.bail(validatorConfig.bail === true ? {} : validatorConfig.bail)(见 src/middlewares/schema.ts#L263-L264),因此bail除了布尔值外,还支持.bail()的完整选项对象,例如{ level: 'request' }表示请求级 bail——不仅停止当前链,还停止当前请求上后续所有校验链的运行。测试 src/middlewares/schema.spec.ts#L396-L412 验证了请求级 bail:第一条链失败后,schema.run(req)只返回了一个结果。

if

为字段的校验器是否继续运行添加条件。等价于链上的.if()。if在当前校验器之前应用,即条件不满足时,该校验器及其后的校验器都不会运行。

checkSchema({ newPassword: { exists: { // 用自定义校验函数作为条件 if: (value, { req }) => !!req.body.oldPassword, // 或者用一条校验链作为条件 if: body('oldPassword').notEmpty(), }, }, });

源码在链上先执行validatorConfig.if && chain.if(validatorConfig.if)再执行该校验器本身(见 src/middlewares/schema.ts#L245)。测试 src/middlewares/schema.spec.ts#L163-L177 验证了当if条件返回 false 时整条链停止执行、无错误产生。

negated

取反校验器的结果。等价于链上的.not()。

checkSchema({ password: { // 检查 password 不为空 isEmpty: { negated: true }, }, });

源码中validatorConfig.negated && chain.not()同样在调用校验器之前执行(见 src/middlewares/schema.ts#L246)。测试 src/middlewares/schema.spec.ts#L230-L241 表明isEmpty: { negated: true }遇到空字符串时会报错。

errorMessage{#validator-errormessage}

为该校验器设置错误消息。等价于链上的.withMessage()。

checkSchema({ email: { isEmail: { errorMessage: 'Must be a valid e-mail address', }, }, });
check('email').isEmail().withMessage('Must be a valid e-mail address');

需要注意的是:errorMessage只能作用于校验器。源码只在isStandardValidator/isCustomValidator分支后调用chain.withMessage(...)(见 src/middlewares/schema.ts#L265),清洗器上的errorMessage会被忽略——测试 src/middlewares/schema.spec.ts#L192-L209(issue #548)专门验证了这一点。


内置清洗器(Built-in Sanitizers)

任何ValidationChain上的内置清洗器(如trim、normalizeEmail、escape、whitelist、toInt等)都可以作为字段 schema 的键。

值为true:无参数开启

checkSchema({ query: { trim: true }, });
check('query').trim();

值为对象:带配置开启

清洗器的值也可以是对象,此时它被开启并携带额外配置:

options

与校验器相同:设置清洗器的参数。多个参数时必须是数组,单个参数可以直接传值。

checkSchema({ email: { normalizeEmail: { options: { gmail_remove_subaddress: true }, }, }, });
check('email').normalizeEmail({ gmail_remove_subaddress: true, });

源码中清洗器与校验器走的是同一条配置管线:options同样经过_.castArray后展开传给清洗器(见 src/middlewares/schema.ts#L249-L252)。测试 src/middlewares/schema.spec.ts#L135-L161 验证了whitelist: { options: ['a'] }会把字段值清洗为'a'。


字段 Schema 修饰符

以下属性可以在字段 schema 中指定,用于修改该字段的通用行为。它们来自源码中的BaseParamSchema(见 src/middlewares/schema.ts#L92-L113),并作为protectedNames('errorMessage'、'in'、'optional',见 src/middlewares/schema.ts#L148)在遍历时被跳过、不会当作校验器/清洗器处理。

in

定义该字段在哪些位置(request location)被校验。例如校验字段存在于 body 或 query 字符串中:

checkSchema({ field: { in: ['body', 'query'], exists: true, }, });

in可以是单个位置字符串,也可以是位置数组;它优先于checkSchema()的defaultLocations参数。测试 src/middlewares/schema.spec.ts#L53-L71 分别验证了in: 'body'与in: ['params', 'body']两种写法对locations的影响。合法的位置值是Location类型:'body' | 'cookies' | 'headers' | 'params' | 'query'(见 docs/api/misc.md#location)。

errorMessage{#field-errormessage}

设置字段的默认错误消息,仅在某个校验器没有在自己的配置中指定errorMessage时使用。等价于check(field, message)的第二个参数。

checkSchema({ password: { errorMessage: 'The password must be at least 8 characters, and must contain a symbol', isLength: { options: { min: 8 } }, matches: { options: /[-_$#]/ }, }, });
check('password', 'The password must be at least 8 characters, and must contain a symbol') .isLength({ min: 8 }) .matches(/[-_$#]/);

源码中config.errorMessage被传给createChain(即check()),从而作为ContextBuilder的默认消息(见 src/middlewares/schema.ts#L214-L218 与 src/middlewares/check.ts#L12-L21)。测试 src/middlewares/schema.spec.ts#L17-L25 验证了字段级errorMessage会体现在生成的 context 消息中。

optional

在字段上设置可选修饰符。等价于链上的.optional()。

checkSchema({ query: { optional: true, isLength: { options: { min: 3 } }, }, });
check('query').optional().isLength({ min: 3 });

optional的值可以是布尔值,也可以是一个带options的对象。源码中通过chain.optional(config.optional === true ? true : config.optional.options)处理(见 src/middlewares/schema.ts#L221-L223),并且注释明确指出“optional 在链中的位置无关紧要”。options支持{ values: 'undefined' | 'null' | 'falsy', nullable, checkFalsy }(其中nullable/checkFalsy为已弃用别名)。测试 src/middlewares/schema.spec.ts#L211-L228 验证了optional: true产生'undefined'级别、optional: { options: { checkFalsy: true, nullable: true } }产生'falsy'级别。


自定义校验器/清洗器(Custom validators)

使用checkSchema()定义自定义校验器或清洗器有两种方式。

方式一:custom/customSanitizer键

在字段 schema 中直接设置custom或customSanitizer。它们与内置校验器/清洗器在 schema 中的用法完全一致,同样支持options、bail、if、negated、errorMessage等配置(对应源码类型CustomValidatorSchemaOptions,见 src/middlewares/schema.ts#L57-L62):

checkSchema({ email: { custom: { options: checkIfEmailExists, bail: true, }, customSanitizer: { options: removeEmailAttribute, }, }, });

等价于:

check('email').custom(checkIfEmailExists).bail().customSanitizer(removeEmailAttribute);

这种方式虽然可行,但每个字段只能设置一个custom和一个customSanitizer。原因很简单:JavaScript 对象不允许重复键(可以重复写,但只有最后一个生效)。如果你想在一个字段上挂多个自定义校验器/清洗器,方式二就是为此设计的。

方式二:任意命名键包裹custom/customSanitizer

在字段 schema 中设置一个既不是内置校验器、也不是内置清洗器、也不是修饰符的任意键名,其值必须是一个包含单个custom或customSanitizer函数的对象。

上面的例子可以改写为:

checkSchema({ email: { emailNotInUse: { custom: checkEmailNotInUse, bail: true, }, removeEmailAttribute: { customSanitizer: removeEmailAttribute, }, }, });

这样同一个字段上就可以挂多个自定义校验器/清洗器了。源码中的识别逻辑是四步类型守卫(见 src/middlewares/schema.ts#L164-L209):先判断是否是标准校验器(isStandardValidator)、标准清洗器(isStandardSanitizer)、再判断是否是自定义校验器(isCustomValidator,键值对象且含custom函数)、自定义清洗器(isCustomSanitizer)。对于未知的键,源码会输出警告并跳过(见 src/middlewares/schema.ts#L236-L238),相关行为由测试 src/middlewares/schema.spec.ts#L103-L116 覆盖。

:::info

自定义校验器/清洗器的名称不会被checkSchema()使用,不同 schema 之间使用相同的自定义名称不会产生冲突。

:::

需要特别注意:不能在内置校验器/清洗器的键下放置custom/customSanitizer——例如isInt: { custom: fn }会被当作isInt的标准配置处理,custom函数不会被执行(见测试 src/middlewares/schema.spec.ts#L262-L278 与 src/middlewares/schema.spec.ts#L299-L315)。


值得留意的边界行为与陷阱

结合源码与测试,以下几个行为值得在实际使用中注意:

  1. 值为false的键会被跳过:schema 中isInt: false这样的“显式关闭”写法不会产生警告,也不会加入链(见 src/middlewares/schema.spec.ts#L88-L101)。
  2. 未知键会触发 console 警告:isBla: true之类的拼写错误或不受支持的键会打印express-validator: schema of "..." has unknown validator/sanitizer "..."并被跳过(见 src/middlewares/schema.ts#L236-L238)。
  3. not与withMessage不能作为 schema 键:它们分别用negated与errorMessage替代(见 src/middlewares/schema.spec.ts#L118-L133)。
  4. falsy 的options值会正常传递:例如default: { options: 0 }会把0正确传给清洗器(见 src/middlewares/schema.spec.ts#L429-L441)。
  5. optional不受链位置影响:无论写在 schema 的哪里,它都影响整个字段对值的解释方式(源码 src/middlewares/schema.ts#L220-L223 中在遍历键之前就处理了optional)。

总结

checkSchema()是 express-validator 中把“多条校验链”浓缩成“一个声明式对象”的入口:

  • 字段选择由字段路径语法(.、[]、*、**)完成;
  • 校验与清洗由内置的 validators/sanitizers(值为true或带options的对象)完成;
  • 行为微调由bail、if、negated、errorMessage、in、optional等修饰符完成;
  • 自定义逻辑通过custom/customSanitizer或任意命名键包裹的方式注入;
  • 返回的ValidationChain[] & ContextRunner既能直接作为中间件使用,也能通过.run(req)手动执行,与oneOf()、checkExact()、validationResult()、matchedData()等 API 组合出完整的请求校验方案。

如果你想进一步深挖,可以从 src/middlewares/schema.ts 的createCheckSchema工厂函数入手,理解 schema 是如何被逐键翻译为链上调用序列的;src/middlewares/schema.spec.ts 则是验证这些行为的最佳参考。

  • 后端

【免费下载链接】express-validator

An express.js middleware for validator.js.

项目地址:https://gitcode.com/gh_mirrors/ex/express-validator
点击查看免费下载
上一篇:Infer 问题抑制机制全解析:@infer-ignore 与 @infer-ignore-every 的使用、通配符与实现原理
下一篇:EOS 钱包导入格式(WIF)规范:私钥编码、解码校验与 keosd 钱包实践

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 2:02:22

框选解释插件dsh:原理、配置与实现

接手一段遗留代码时&#xff0c;我们几乎都经历过同一个瞬间&#xff1a;光标落在一个足有 80 行的函数上&#xff0c;里面嵌套着两层 for 循环&#xff0c;一行正则直接连写到底。你想快速知道这段逻辑到底在做什么。于是&#xff0c;选中代码、CtrlC、切到聊天窗口、CtrlV、补…

作者头像 李华
网站建设 2026/10/10 2:00:02

可靠性密码 | 高可靠性之光学设计与制程管控(上)

△ 高可靠性固体激光器激光技术飞速发展的当下&#xff0c;固体激光器凭借其高功率、高效率、长寿命等优势&#xff0c;在工业加工、医疗美容等领域占据重要地位。然而&#xff0c;随着应用场景的日益复杂和严苛&#xff0c;对激光器的可靠性要求也愈发严格。光学系统作为激光器…

作者头像 李华
网站建设 2026/10/10 1:59:37

海康iSecure Center生产级部署:从环境校准到服务验证

简介&#xff1a;本资源是一份面向安防系统集成工程师、IT运维人员及弱电项目实施人员的海康威视iSecure Center综合安防平台&#xff08;含视频监控、门禁管理、报警管理&#xff09;全流程部署实操指南&#xff0c;聚焦生产环境落地难点&#xff0c;解决从零搭建平台时的环境…

作者头像 李华
网站建设 2026/10/10 1:57:51

SMP/NUMA/PER_CPU

whywhathow PER_CPU 从上图中我们可以看到&#xff0c;各种源文件中 静态percpu变量 通过DEFINE_PER_CPU的方式&#xff0c;定义了很多percpu变量&#xff0c;这些变量根据vmlinux.lds.S中的相关定义&#xff0c;会被linker聚合在一起&#xff0c;然后放到最终vmlinux文件的&…

作者头像 李华