H3 数据校验
服务端收到的每一份数据——来自用户、来自外部 API——都不可信。校验(validation)就是确认「数据的形状符合预期」这一关,H3 为此提供了一组 *Validated* 工具。
类型泛型 ≠ 校验
⚠️ 第一原则:类型泛型不是校验。给
readBody<T&传一个接口,只是给编辑器看的提示,运行时并不会真的检查数据。你必须显式校验后再使用。
三个校验工具
H3 不自带校验库,但支持 Standard-Schema 规范兼容的 schema(Zod、Valibot、ArkType 等)。对应三种入参各有替换品:
| 入参 | 不校验 | 校验版 |
|---|---|---|
| 路由参数 | getRouterParams |
getValidatedRouterParams |
| 查询串 | getQuery |
getValidatedQuery |
| 请求体 | readBody |
readValidatedBody |
校验失败会直接抛 400 Validation Error,错误数据里带有具体的校验问题,可以原样回给客户端做友好提示。
校验路由参数
import { getValidatedRouterParams } from "h3";
import { z } from "zod";
// 用 Zod 描述期望的形状
const contentSchema = z.object({
topic: z.string().min(1), // 非空字符串
uuid: z.string().uuid(), // 合法 UUID
});
app.all("/content/:topic/:uuid", async (event) => {
// 校验失败会抛 400,成功则返回带类型的 params
const params = await getValidatedRouterParams(event, contentSchema);
return `正在查看 topic="${params.topic}" uuid="${params.uuid}"`;
});
注意:用参数就必须走路由(:topic、:uuid),getRouterParams 那一节讲过原理。
校验查询串,并顺手转换类型
查询串天然是字符串,校验库还能顺便做类型转换——比如把 "2" 变成数字 2,对分页特别有用:
import { getValidatedQuery } from "h3";
import { z } from "zod";
// 先把数字字符串转成真正的 number
const stringToNumber = z
.string()
.regex(/^\d+$/, "必须是数字字符串")
.transform(Number);
const paginationSchema = z.object({
page: stringToNumber.optional().default(1), // 默认第 1 页
size: stringToNumber.optional().default(10), // 默认每页 10 条
});
app.use(async (event) => {
const query = await getValidatedQuery(event, paginationSchema);
return `第 ${query.page} 页,每页 ${query.size} 条`;
});
相比参数校验,这里多演示了一层 transform:入参是字符串,出参已是数字。
校验请求体
import { readValidatedBody } from "h3";
import { z } from "zod";
const userSchema = z.object({
name: z.string().min(3).max(20), // 3~20 字符
age: z.number({ coerce: true }).positive().int(), // 正整数(coerce 尝试把字符串转数字)
});
app.use(async (event) => {
const body = await readValidatedBody(event, userSchema);
return `你好 ${body.name},你 ${body.age} 岁`;
});
合法的 JSON 体 { "name": "John", "age": 42 } 会得到 你好 John!你 42 岁;不合法则抛 400。
自己处理错误:safeParse 分支
默认直接抛 400 足够简单,但有时你想自己决定错误响应长什么样。把校验库的 safe 解析函数作为第二个参数传入即可:
import { getValidatedRouterParams } from "h3";
import { z } from "zod/v4";
const contentSchema = z.object({
topic: z.string().min(1),
uuid: z.string().uuid(),
});
app.all("/content/:topic/:uuid", async (event) => {
// 传入 .safeParse 而不是 schema 本身,H3 就不会抛错,而是把结果交给你
const result = await getValidatedRouterParams(event, contentSchema.safeParse);
if (!result.success) {
// 自己决定返回什么——注意不要把内部 schema 细节直接暴露给客户端
return `校验失败:\n${z.prettifyError(result.error)}`;
}
return `正在查看 topic="${result.data.topic}" uuid="${result.data.uuid}"`;
});
Valibot 对应的是 v.safeParser(contentSchema)。
💡 别把内部结构泄露给客户端:上面的
prettifyError只是演示可读性。生产环境应把校验错误收敛成「字段名 + 中文提示」之类的稳定文案,避免把你的 schema 构造、字段规则原样回传,那等于给攻击者画了一张数据模型地图。
原型污染风险:readBody 不再自动反序列化
⚠️ 官方特别强调:新版本
readBody不再对体做destr风格的自动反序列化与对象展开,因此你拿到的就是一个普通对象——但这也意味着没有任何一层替你挡掉原型污染。{ "__proto__": { ... } }这类体一旦被Object.assign/ 展开运算符合并进你的对象,就可能污染全局原型。务必:用上面这些 Validated 工具(它们基于 schema 白名单取值,天然只取声明的字段),而不要拿原始readBody结果直接合并进业务对象。