区别于 Express/Koa 需要额外引入 joi/zod 等校验库,Fastify 把 JSON Schema 校验与预编译响应序列化内建到路由系统里。它不仅是运行时安全校验,更是 Fastify 高性能的核心来源之一。
前置知识:先阅读 Fastify 路由与 Handler。
| 能力 | 说明 |
|---|---|
| 入参校验 | 对 params/query/body/headers 做类型与结构校验 |
| 类型强制 | 通过 coerceTypes 将字符串自动转为目标类型 |
| 响应序列化 | 用 fast-json-stringify 预编译序列化函数,跳过运行时反射 |
| 文档化 | 校验规则本身可作为 OpenAPI 文档来源(配合 @fastify/swagger) |
接入 Schema 后,序列化函数被提前编译为专用代码,通常能带来明显的吞吐提升(具体幅度取决于负载特征,建议以自身压测为准)。
app.post('/users', {
schema: {
// 请求体校验
body: {
type: 'object',
required: ['name'],
properties: {
name: { type: 'string', minLength: 2, maxLength: 20 },
age: { type: 'integer', minimum: 0, maximum: 120 },
},
additionalProperties: false,
},
// 查询参数
querystring: {
type: 'object',
properties: { page: { type: 'integer', default: 1 } },
},
// 路径参数
params: {
type: 'object',
properties: { id: { type: 'string' } },
required: ['id'],
},
// 响应序列化(key 是状态码)
response: {
200: {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' },
},
required: ['id', 'name'],
},
4xx: {
type: 'object',
properties: { message: { type: 'string' } },
},
},
},
handler: async (req) => {
return { id: Date.now(), name: req.body.name };
},
});校验失败时,Fastify 自动返回 400,响应体形如:
{
"statusCode": 400,
"code": "FST_ERR_VALIDATION",
"error": "Bad Request",
"message": "body/name must NOT have fewer than 2 characters"
}Fastify 内置 AJV,可在创建实例时配置:
import Fastify from 'fastify';
import Ajv from 'ajv'; // 可选:自定义实例
import addFormats from 'ajv-formats';
const app = Fastify({
ajv: {
customOptions: {
coerceTypes: true, // 字符串自动转类型
removeAdditional: true, // 删除未声明字段
allErrors: true, // 收集所有错误而非只报第一个
},
plugins: [addFormats], // 启用 format 校验(email/date-time 等)
},
});format 校验启用 ajv-formats 后可在 schema 中使用格式约束:
schema: {
body: {
type: 'object',
required: ['email', 'birthday'],
properties: {
email: { type: 'string', format: 'email' },
birthday: { type: 'string', format: 'date-time' },
},
},
}coerceTypes 示例// 请求 /users?page=3 → req.query.page 自动为数字 3
querystring: {
type: 'object',
properties: { page: { type: 'integer' } },
}当声明了 schema.response[statusCode],Fastify 会把该 schema 交给 fast-json-stringify 编译成专用序列化函数,缓存复用;没有声明的路由则走通用序列化。这意味着:
response: {
200: { type: 'object', properties: { id: { type: 'integer' } } },
201: { type: 'object', properties: { id: { type: 'integer' } } },
default: { type: 'object', properties: { message: { type: 'string' } } },
}default 兜底所有未显式声明的状态码。若匹配不到也非 default,则退回通用序列化。
每个路由可单独指定序列化逻辑:
app.get('/user', {
schema: { response: { 200: { type: 'object' } } },
serializerCompiler: ({ schema }) => {
// 返回一个序列化函数
return (data) => JSON.stringify(data);
},
handler: async () => ({ id: 1, name: 'A', secret: 'x' }),
});序列化 schema 只列出要返回的字段即可实现「字段裁剪」,例如隐藏内部字段:
response: {
200: {
type: 'object',
properties: { id: { type: 'integer' }, name: { type: 'string' } },
},
}
// 即使 handler 返回 { id, name, passwordHash },passwordHash 也不会出现在响应中app.addSchema() 全局注册复用片段,配合 $ref 引用,减少重复声明:app.addSchema({
$id: 'User',
type: 'object',
properties: { id: { type: 'integer' }, name: { type: 'string' } },
});
app.get('/users/:id', {
schema: {
params: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
response: { 200: { $ref: 'User#' } },
},
handler: async (req) => ({ id: Number(req.params.id), name: 'Alice' }),
});有时需要业务化的条件校验,可在 body 等 schema 上用 $data 关键字,或直接在校验器中写函数:
const app = Fastify({
schemaErrorFormatter(errors, dataVar) {
return new Error(`校验失败 ${dataVar}: ${errors[0].message}`);
},
});更常见的是在 Handler 前的 preValidation 钩子里做自定义逻辑(见 钩子篇)。
src/
├── schemas/
│ ├── user.schema.js
│ ├── order.schema.js
│ └── common.js # 公共 $id 定义可结合 json-schema-to-ts 从 schema 推断出 TypeScript 类型,保证请求体类型与校验规则一致:
import type { FromSchema } from 'json-schema-to-ts';
const createUserSchema = {
type: 'object',
properties: { name: { type: 'string' } },
} as const;
type CreateUserBody = FromSchema<typeof createUserSchema>;
// type CreateUserBody = { name?: string }@fastify/swagger 会读取每个路由的 schema 自动生成 OpenAPI 文档,实现「一处定义、校验与文档共用」。
additionalProperties 被保留在请求体里?需在 ajv.customOptions 中开启 removeAdditional: true,否则只是「忽略」而非「删除」。想直接拒绝额外字段,则保留默认并依靠校验错误提示。
req.query.page 为什么是字符串?未开启 coerceTypes: true。开启后声明 type: 'integer' 即可自动转换。
覆盖实例级 schemaErrorFormatter,或写成统一错误处理中介(见 日志与错误处理篇)。