Fastify 流畅模式:用 fluent-json-schema 编写可复用的请求校验与响应序列化 Schema
Fastify 支持通过 JSON Schema 对请求的 body、params、query、headers 做校验,并对响应做高性能序列化。本指南讲解如何结合官方推荐的 fluent-json-schema 以链式 API 方式声明这些 Schema(无需手写 JSON、无需手动调用 .valueOf()),并通过 addSchema() 将共享 Schema 注册到 Fastify 实例中以实现复用。读完本文,你将掌握 fluent schema 的完整声明语法、两种共享引用方式($ref-way 与 replace-way)及其在 Fastify 内部的识别与转换机制。
fluent-json-schema 是什么,为什么用它
按照 Validation and Serialization 参考文档,Fastify 的路由 schema 选项接受 body、querystring(或 query)、params、headers、response 等键,其值就是标准 JSON Schema。手写大段 JSON Schema 冗长且难以维护,而 fluent-json-schema(Fastify 生态的官方依赖,见 package.json 中 fluent-json-schema: ^6.0.0)提供了链式声明式写法,同时允许复用常量(如枚举值、公共子结构),这正是本指南的主题。
基础用法:一个路由的完整 fluent schema 声明
以下示例覆盖了 body、query、params、headers 四个维度的常用 fluent 方法:
const S = require('fluent-json-schema')
// You can have an object like this, or query a DB to get the values
const MY_KEYS = {
KEY1: 'ONE',
KEY2: 'TWO'
}
const bodyJsonSchema = S.object()
.prop('someKey', S.string())
.prop('someOtherKey', S.number())
.prop('requiredKey', S.array().maxItems(3).items(S.integer()).required())
.prop('nullableKey', S.mixed([S.TYPES.NUMBER, S.TYPES.NULL]))
.prop('multipleTypesKey', S.mixed([S.TYPES.BOOLEAN, S.TYPES.NUMBER]))
.prop('multipleRestrictedTypesKey', S.oneOf([S.string().maxLength(5), S.number().minimum(10)]))
.prop('enumKey', S.enum(Object.values(MY_KEYS)))
.prop('notTypeKey', S.not(S.array()))
const queryStringJsonSchema = S.object()
.prop('name', S.string())
.prop('excitement', S.integer())
const paramsJsonSchema = S.object()
.prop('par1', S.string())
.prop('par2', S.integer())
const headersJsonSchema = S.object()
.prop('x-foo', S.string().required())
// Note that there is no need to call `.valueOf()`!
const schema = {
body: bodyJsonSchema,
querystring: queryStringJsonSchema, // (or) query: queryStringJsonSchema
params: paramsJsonSchema,
headers: headersJsonSchema
}
fastify.post('/the/url', { schema }, handler)
几个关键要点:
- 无需调用
.valueOf():fluent schema 对象本身可以像普通 JSON Schema 一样直接放进schema选项。这一点并非偶然,Fastify 内部会检测并转换(原理见下节),因此测试文件 test/fluent-schema.test.js 中有专门的用例Should call valueOf internally覆盖query、querystring、body、params、headers以及response.200/201全部位置。 querystring与query等价:两者只能出现其一,同时出现会抛出FST_ERR_SCH_DUPLICATE('querystring')错误。- 常量复用:
S.enum(Object.values(MY_KEYS))演示了把业务常量(甚至来自数据库的值)直接映射为枚举约束,避免魔法字符串散落在 schema 中。 - 多类型表达:
S.mixed([...])声明"任意类型之一"(如可空字段nullableKey),S.oneOf([...])声明"各分支带不同约束"(如string 且 ≤5或number 且 ≥10),S.not(S.array())做类型排除。
同样的写法也可以用于 response,实现响应序列化。官方测试给出了一个完整形态:
fastify.post('/:id', {
handler: (req, reply) => { reply.send({ name: 'a', surname: 'b', dateOfBirth: '01-01-2020' }) },
schema: {
params: S.object().prop('id', S.integer().minimum(42)),
headers: S.object().prop('x-custom', S.string().format('email')),
query: S.object().prop('surname', S.string().required()),
body: S.object().prop('name', S.string().required()),
response: {
200: S.object()
.prop('name', S.string())
.prop('surname', S.string())
}
}
})
从该测试(test/fluent-schema.test.js)可以看到两点实战价值:一是校验失败时 Fastify 返回结构化错误,如 params/id must be >= 42、headers/x-custom must match format "email";二是 response 序列化会裁剪掉未在 schema 中声明的字段——handler 返回了 dateOfBirth,但 200 响应体中只有 name 和 surname。
源码机制:Fastify 如何识别并转换 fluent schema
从源码结构看,fastify 对 fluent 对象的"透明支持"由 lib/schemas.js 实现:
// lib/schemas.js
const kFluentSchema = Symbol.for('fluent-schema-object')
function generateFluentSchema (schema) {
for (const key of SCHEMAS_SOURCE) {
if (schema[key] && (schema[key].isFluentSchema || schema[key][kFluentSchema])) {
schema[key] = schema[key].valueOf()
}
}
if (schema.response) {
const httpCodes = Object.keys(schema.response)
for (const code of httpCodes) {
if (schema.response[code].isFluentSchema || schema.response[code][kFluentSchema]) {
schema.response[code] = schema.response[code].valueOf()
}
}
}
}
其工作机制是:
- 双版本兼容识别:通过
isFluentSchema(fluent-json-schemav6 的标记属性)或全局符号Symbol.for('fluent-schema-object')(旧版fluent-schema的标记)判断对象是否为 fluent schema。从源码结构看,这使 fastify 同时兼容新旧两代 fluent schema 库。 - 按需
valueOf()展开:只有被识别为 fluent 对象时才调用.valueOf()得到纯 JSON Schema 对象;SCHEMAS_SOURCE数组(params、body、querystring、query、headers)与response的各状态码均被遍历处理。这就是文档中"无需手动.valueOf()"承诺的实现来源。 - 别名归一化:
normalizeSchema()会把query键改写为querystring键,所以两种写法最终等价。
转换之后的纯 JSON Schema 会交给 Fastify 的 SchemaController 编译:默认由 @fastify/ajv-compiler 构建校验器、@fastify/fast-json-stringify-compiler 构建序列化器(见 lib/schema-controller.js)。也就是说,fluent 写法最终走的仍是 Fastify 标准的校验/序列化管线,不改变任何性能特征。
复用共享 schema:addSchema 的两种引用方式
当某个子结构(如地址)需要在多个路由间复用时,fluent-json-schema 允许更轻松地以编程方式操纵 schema,再通过 fastify.addSchema() 注册复用。Validation and Serialization 参考文档 详细定义了引用的解析规则,fluent 场景下有典型两种用法。
方式一:$ref-way(外部 schema 引用)
用一个带 $id 的"容器对象"把若干命名 schema 以 definitions 形式登记,然后路由中用 S.ref() 指向其中某个定义:
const addressSchema = S.object()
.id('#address')
.prop('line1').required()
.prop('line2')
.prop('country').required()
.prop('city').required()
.prop('zipcode').required()
const commonSchemas = S.object()
.id('https://fastify/demo')
.definition('addressSchema', addressSchema)
.definition('otherSchema', otherSchema) // You can add any schemas you need
fastify.addSchema(commonSchemas)
const bodyJsonSchema = S.object()
.prop('residence', S.ref('https://fastify/demo#address')).required()
.prop('office', S.ref('https://fastify/demo#/definitions/addressSchema')).required()
const schema = { body: bodyJsonSchema }
fastify.post('/the/url', { schema }, handler)
两个 $ref 的写法对应两种解析路径:https://fastify/demo#address 在共享 schema 内部查找 $id: '#address' 的子 schema;https://fastify/demo#/definitions/addressSchema 则直接取 definitions.addressSchema。注意 S.object().id('#address') 生成的就是文档中 myField: { $ref: 'http://url.com/sh.json#foo' } 规则所要求的内部 $id。
方式二:replace-way(验证前整段替换)
也可以不走 $ref 语义,而是把一个"整体共享 schema"以 $id 注册,路由 schema 中用字符串形式引用其根,在校验前被 Fastify 替换为完整 schema 内容:
const sharedAddressSchema = {
$id: 'sharedAddress',
type: 'object',
required: ['line1', 'country', 'city', 'zipcode'],
properties: {
line1: { type: 'string' },
line2: { type: 'string' },
country: { type: 'string' },
city: { type: 'string' },
zipcode: { type: 'string' }
}
}
fastify.addSchema(sharedAddressSchema)
const bodyJsonSchema = {
type: 'object',
properties: {
vacation: 'sharedAddress#'
}
}
const schema = { body: bodyJsonSchema }
fastify.post('/the/url', { schema }, handler)
此方式与 fluent 无关——共享 schema 可以直接手写为普通 JSON Schema 对象,addSchema() 对两种形态一视同仁。replace-way 也完全可以在 fluent 之外与原生写法混用,test/shared-schema 示例即演示了共享 schema 的基本用法。
两种方式的混合与底层存储机制
ℹ️ 说明:使用
fastify.addSchema时,$ref-way和replace-way可以任意混合。
这一混合能力有专门的回归测试 test/fluent-schema.test.js 用例 use fluent schema and plain JSON schema 佐证:同一个 Fastify 实例上先注册 fluent 风格的 commonSchemas,再注册普通对象风格的 sharedAddressSchema,两条路由均能成功通过 fastify.ready() 的编译。
从源码结构看,addSchema() 的落地存储在 lib/schemas.js:
Schemas.prototype.add = function (inputSchema) {
const schema = fastClone((inputSchema.isFluentSchema || inputSchema.isFluentJSONSchema || inputSchema[kFluentSchema])
? inputSchema.valueOf()
: inputSchema
)
// developers can add schemas without $id, but with $def instead
const id = schema.$id
if (!id) {
throw new FST_ERR_SCH_MISSING_ID()
}
if (this.store[id]) {
throw new FST_ERR_SCH_ALREADY_PRESENT(id)
}
this.store[id] = schema
}
要点:
- fluent 容器同样免
.valueOf():如果addSchema()直接传入 fluent 对象,内部会先valueOf()展开,再深拷贝(rfdc)入 store; - 强制
$id:共享 schema 缺$id会抛FST_ERR_SCH_MISSING_ID,同一$id重复注册抛FST_ERR_SCH_ALREADY_PRESENT; - 封装性:
addSchemaAPI 是 encapsulated 的,只在当前 Fastify 上下文(及其子上下文)内可见,配合插件体系可以做出模块内私有的共享 schema。
此外,如果自定义了校验器/序列化器,.addSchema 不再由 Fastify 代管,需要用 .getSchemas() / .getSchema(schemaId) 读取已注册的 schema(详见 Validation and Serialization 参考文档)。
小结与实践建议
- 路由级 schema 统一使用
S.object().prop(...)链式写法,枚举、格式约束(如S.string().format('email'))全部声明在 fluent 层,常量(枚举值等)从外部变量注入,便于从数据库或配置中心取值; body、params、query(或querystring)、headers、response.<状态码>五个位置都可以直接放 fluent 对象,Fastify 会在 lib/schemas.js 中自动valueOf(),不要手写转换逻辑;- 复用优先选择
$ref-way+S.ref():引用粒度细、语义即标准 JSON Schema$ref,方便被 Swagger 等工具理解;把整个对象原样塞给某个字段时才考虑replace-way; - 校验/序列化失败会走 Fastify 标准错误通道(
FST_ERR_VALIDATION等),行为可参考 test/fluent-schema.test.js 中对 400 响应体的逐项断言,便于编写端到端测试。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00