04 错误结果与 matchedData
1. validationResult()
验证链不会自动结束请求。使用下面的函数读取当前请求积累的错误:
import { validationResult } from "express-validator";
const errors = validationResult(req);
返回值是 Result<ValidationError>,常用方法包括:
isEmpty()
array()
mapped()
formatWith()
throw()
2. .isEmpty() 与 .array()
const errors = validationResult(req);
if (!errors.isEmpty()) {
res.status(400).json({ errors: errors.array() });
return;
}
array() 返回错误数组。设置 onlyFirstError 可以每个字段只保留第一个错误:
errors.array({ onlyFirstError: true });
它与 .bail({ level: "request" }) 不同:
onlyFirstError只改变错误输出;所有校验可能已经执行;bail会真正阻止后续 validator 执行。
3. .mapped()
const mappedErrors = errors.mapped();
它按字段路径生成对象,适合表单字段错误:
{
"name": {
"type": "field",
"msg": "name 不能为空",
"path": "name",
"location": "body"
}
}
同一字段存在多个错误时,只会保留一个映射值。如果需要展示全部错误,使用 array()。
4. 错误类型
express-validator 7 的错误是可区分联合类型,常见类型包括:
FieldValidationError
AlternativeValidationError
GroupedAlternativeValidationError
UnknownFieldValidationError
处理字段错误时应先判断 type:
import type { ValidationError } from "express-validator";
function formatError(error: ValidationError) {
if (error.type === "field") {
return {
field: error.path,
location: error.location,
message: String(error.msg),
};
}
return {
message: String(error.msg),
};
}
不能假设所有错误都有 path 和 location,因为 oneOf() 和 checkExact() 会产生其他类型。
5. .formatWith()
const formatted = validationResult(req).formatWith((error) => {
if (error.type === "field") {
return {
field: error.path,
message: String(error.msg),
};
}
return {
message: String(error.msg),
};
});
然后:
res.status(400).json({ errors: formatted.array() });
formatWith() 返回一个新的 Result,不会改变原 Result 的泛型类型。
6. 项目级 withDefaults()
import {
validationResult,
type ValidationError,
} from "express-validator";
interface ApiValidationError {
code: string;
field?: string;
message: string;
}
export const appValidationResult = validationResult.withDefaults<
ApiValidationError
>({
formatter(error: ValidationError): ApiValidationError {
if (error.type === "field") {
return {
code: "INVALID_FIELD",
field: error.path,
message: String(error.msg),
};
}
return {
code: "INVALID_REQUEST",
message: String(error.msg),
};
},
});
统一中间件:
export function validateRequest(
req: Request,
res: Response,
next: NextFunction,
): void {
const errors = appValidationResult(req);
if (!errors.isEmpty()) {
res.status(400).json({
code: "VALIDATION_ERROR",
errors: errors.array(),
});
return;
}
next();
}
7. 不要泄露非法值
默认字段错误可能包含 value。如果字段是密码或 Token:
body("password")
.isStrongPassword()
.hide();
统一 formatter 也不应把 error.value 原样返回。日志同样需要脱敏。
8. .throw()
validationResult(req).throw();
存在错误时会抛出异常。它适合统一异常流,但需要确保错误处理中间件能区分验证错误,不要把它当成普通 500。
对于当前学习项目,显式 isEmpty() 判断更容易理解。
9. matchedData() 为什么重要
即使验证了两个字段,req.body 仍可能包含其他内容:
{
"name": "学习 TypeScript",
"content": "完成校验教程",
"isAdmin": true
}
下面的代码有 Mass Assignment 风险:
await Wish.create(req.body);
只提取被验证字段:
import { matchedData } from "express-validator";
interface CreateWishInput {
name: string;
content: string;
}
const input = matchedData<CreateWishInput>(req);
await Wish.create(input);
10. matchedData() options
matchedData(req, {
includeOptionals: false,
onlyValidData: true,
locations: ["body"],
});
includeOptionals
是否包含被 .optional() 选中但未提供的字段。
onlyValidData
默认为 true,只提取通过验证的数据。通常不要改为 false。
locations
限制提取位置:
locations: ["body", "params"]
避免 body 和 query 同名字段混入结果。
11. TypeScript 泛型的边界
const input = matchedData<CreateWishInput>(req);
这个泛型不会自动证明验证规则与接口完全一致。如果校验链漏掉 content,TypeScript 仍可能相信它存在。
因此要保持三者同步:
ValidationChain
DTO interface
Controller/Service 使用方式
对重要接口编写集成测试,验证实际输出结构。
12. 在中间件中挂载可信数据
一种项目实践是验证成功后统一提取:
res.locals.validated = matchedData(req);
Controller 再读取:
const input = res.locals.validated as CreateWishInput;
这需要扩展 ResponseLocals 类型。小项目直接在 Controller 调用 matchedData<T>() 更直观,避免过早抽象。
13. i18n 消息 key
校验规则可以保存稳定 key:
.withMessage("validation.wish.name.required")
formatter 根据请求语言翻译:
message: req.t(String(error.msg))
响应可以同时返回:
{
"code": "validation.wish.name.required",
"field": "name",
"message": "名称不能为空"
}
前端使用 message 展示,code 用于稳定逻辑。
14. 练习题
- 将
errors.array()转换成只包含 field、code、message 的响应。 - 使用
withDefaults()创建项目级 ResultFactory。 - 对 password 使用
.hide(),确认响应中不再出现原密码。 - 用
matchedData()阻止客户端提交额外的isAdmin字段。 - 分别测试
includeOptionals为 true 和 false 的结果。 - 为
oneOf()和checkExact()错误补充非 field 类型的 formatter 分支。