05 checkSchema 声明式校验
1. checkSchema 是什么
ValidationChain 使用链式写法:
body("name")
.isString()
.bail()
.trim()
.isLength({ min: 1, max: 20 });
checkSchema() 使用对象集中描述多个字段:
import { checkSchema } from "express-validator";
export const createWishSchema = checkSchema({
name: {
in: ["body"],
isString: {
errorMessage: "name 必须是字符串",
bail: true,
},
trim: true,
isLength: {
options: { min: 1, max: 20 },
errorMessage: "name 必须是 1 到 20 个字符",
},
},
});
返回结果既能作为 Express 中间件数组使用,也能通过 .run(req) 手动执行。
2. 完整创建接口 Schema
import { checkSchema } from "express-validator";
export const createWishSchema = checkSchema({
name: {
in: ["body"],
exists: {
errorMessage: "必须提供 name",
bail: true,
},
isString: {
errorMessage: "name 必须是字符串",
bail: true,
},
trim: true,
isLength: {
options: {
min: 1,
max: 20,
},
errorMessage: "name 必须是 1 到 20 个字符",
},
},
content: {
in: ["body"],
isString: {
errorMessage: "content 必须是字符串",
bail: true,
},
trim: true,
isLength: {
options: {
min: 1,
max: 200,
},
errorMessage: "content 必须是 1 到 200 个字符",
},
},
});
路由:
router.post(
"/create",
express.json(),
createWishSchema,
validateRequest,
createWish,
);
3. in:明确数据位置
in: ["body"]
in: ["query"]
in: ["params"]
in: ["headers"]
in: ["cookies"]
可以指定多个位置:
in: ["body", "query"]
但同名字段来源会变得不清晰。日常 API 应优先指定单一位置。
也可以给整个 Schema 设置默认位置:
checkSchema(schema, ["body"]);
4. validator options 的写法
没有 options:
isString: true
带 options 和消息:
isInt: {
options: {
min: 1,
max: 100,
},
errorMessage: "必须是 1 到 100 的整数",
bail: true,
}
某些 validator 的参数不是单个对象,需要使用数组传参。具体应对照对应 ValidationChain 方法签名,避免把多参数错误地写成一个对象。
5. sanitizer
const listSchema = checkSchema(
{
page: {
default: {
options: 1,
},
isInt: {
options: { min: 1 },
bail: true,
},
toInt: true,
},
keyword: {
optional: true,
isString: true,
trim: true,
isLength: {
options: { max: 50 },
},
},
},
["query"],
);
和 ValidationChain 一样,sanitizer 的执行顺序会影响后续验证。Schema 适合集中查看配置,但长对象中的执行顺序没有链式代码那么直观,因此复杂转换要格外谨慎。
6. optional
nickname: {
in: ["body"],
optional: {
options: {
values: "undefined",
},
},
isString: true,
trim: true,
}
简单形式:
optional: true
PATCH Schema 经常把字段设为 optional,但通常还需要检查“至少提交一个允许更新的字段”,可以结合 checkExact()、matchedData() 和自定义请求级规则处理。
7. 自定义 validator
endDate: {
in: ["body"],
isISO8601: {
bail: true,
},
custom: {
custom: (endDate: string, { req }) => {
const startDate = String(req.body.startDate);
if (new Date(endDate) < new Date(startDate)) {
throw new Error("endDate 不能早于 startDate");
}
return true;
},
},
}
异步函数同样受支持:
email: {
in: ["body"],
isEmail: { bail: true },
custom: {
custom: async (email: string) => {
const exists = await User.findOne({ where: { email } });
if (exists !== null) {
throw new Error("邮箱已存在");
}
return true;
},
},
}
8. 自定义 sanitizer
tags: {
in: ["body"],
customSanitizer: {
customSanitizer: (value: unknown) =>
String(value)
.split(",")
.map((item) => item.trim()),
},
isArray: {
options: { max: 10 },
},
}
9. Wildcard 和 Globstar
const orderSchema = checkSchema({
items: {
in: ["body"],
isArray: {
options: { min: 1, max: 50 },
},
},
"items.*.productId": {
in: ["body"],
isInt: {
options: { min: 1 },
},
toInt: true,
},
"items.*.quantity": {
in: ["body"],
isInt: {
options: { min: 1, max: 100 },
},
toInt: true,
},
});
teams.**.name 可匹配任意深度字段,但必须限制树形输入规模。
10. 手动运行
const result = await createWishSchema.run(req);
if (!result.isEmpty()) {
// 处理错误。
}
checkSchema() 返回 RunnableValidationChains,除整体运行外还可以访问其中的链。常规路由仍建议把它直接作为中间件使用。
11. 与 ValidationChain 的选择
| 场景 | ValidationChain | checkSchema |
|---|---|---|
| 少量简单字段 | 更直观 | 配置显得偏长 |
| 大型表单 | 链数组较长 | 字段规则集中 |
| 复杂条件 | .if() 更易读 |
对象嵌套容易变深 |
| 复用配置 | 使用函数工厂 | 可以组合 Schema 对象 |
| 执行顺序 | 一眼可见 | 需要更仔细阅读 |
| DTO 自动推导 | 不支持 | 同样不支持 |
不要为了追求“配置化”把所有接口都改成 Schema。链式写法和 Schema 可以在同一项目中并存,但团队最好建立选择约定。
12. Schema 组合风险
可以使用对象展开:
import type { Schema } from "express-validator";
const timestampsSchema = {
createdAt: {
in: ["query"],
isISO8601: true,
},
} satisfies Schema;
但字段覆盖是普通 JavaScript 对象覆盖。多个 Schema 出现同名 key 时,后面的对象会完全替换前面的字段配置,而不是深度合并。
13. 练习题
- 把当前
createWishValidation改写成 checkSchema,但不要修改路由。 - 使用默认位置
["query"]编写分页 Schema。 - 为订单 items 数组编写带 wildcard 的 Schema。
- 编写 PATCH 用户资料 Schema,所有字段可选,但出现时必须合法。
- 在 Schema 中添加异步邮箱查重,并使用 bail 避免无效查询。
- 比较相同接口的 ValidationChain 和 checkSchema 版本,记录哪一种更容易看出执行顺序。