首页
/ Fastify 流畅模式:用 fluent-json-schema 编写可复用的请求校验与响应序列化 Schema

Fastify 流畅模式:用 fluent-json-schema 编写可复用的请求校验与响应序列化 Schema

2026-09-05 18:23:47作者:侯霆垣

Fastify 支持通过 JSON Schema 对请求的 body、params、query、headers 做校验,并对响应做高性能序列化。本指南讲解如何结合官方推荐的 fluent-json-schema 以链式 API 方式声明这些 Schema(无需手写 JSON、无需手动调用 .valueOf()),并通过 addSchema() 将共享 Schema 注册到 Fastify 实例中以实现复用。读完本文,你将掌握 fluent schema 的完整声明语法、两种共享引用方式($ref-wayreplace-way)及其在 Fastify 内部的识别与转换机制。

fluent-json-schema 是什么,为什么用它

按照 Validation and Serialization 参考文档,Fastify 的路由 schema 选项接受 bodyquerystring(或 query)、paramsheadersresponse 等键,其值就是标准 JSON Schema。手写大段 JSON Schema 冗长且难以维护,而 fluent-json-schema(Fastify 生态的官方依赖,见 package.jsonfluent-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 覆盖 queryquerystringbodyparamsheaders 以及 response.200/201 全部位置。
  • querystringquery 等价:两者只能出现其一,同时出现会抛出 FST_ERR_SCH_DUPLICATE('querystring') 错误。
  • 常量复用S.enum(Object.values(MY_KEYS)) 演示了把业务常量(甚至来自数据库的值)直接映射为枚举约束,避免魔法字符串散落在 schema 中。
  • 多类型表达S.mixed([...]) 声明"任意类型之一"(如可空字段 nullableKey),S.oneOf([...]) 声明"各分支带不同约束"(如 string 且 ≤5number 且 ≥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 >= 42headers/x-custom must match format "email";二是 response 序列化会裁剪掉未在 schema 中声明的字段——handler 返回了 dateOfBirth,但 200 响应体中只有 namesurname

源码机制: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()
      }
    }
  }
}

其工作机制是:

  1. 双版本兼容识别:通过 isFluentSchemafluent-json-schema v6 的标记属性)或全局符号 Symbol.for('fluent-schema-object')(旧版 fluent-schema 的标记)判断对象是否为 fluent schema。从源码结构看,这使 fastify 同时兼容新旧两代 fluent schema 库。
  2. 按需 valueOf() 展开:只有被识别为 fluent 对象时才调用 .valueOf() 得到纯 JSON Schema 对象;SCHEMAS_SOURCE 数组(paramsbodyquerystringqueryheaders)与 response 的各状态码均被遍历处理。这就是文档中"无需手动 .valueOf()"承诺的实现来源。
  3. 别名归一化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-wayreplace-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
  • 封装性addSchema API 是 encapsulated 的,只在当前 Fastify 上下文(及其子上下文)内可见,配合插件体系可以做出模块内私有的共享 schema。

此外,如果自定义了校验器/序列化器,.addSchema 不再由 Fastify 代管,需要用 .getSchemas() / .getSchema(schemaId) 读取已注册的 schema(详见 Validation and Serialization 参考文档)。

小结与实践建议

  • 路由级 schema 统一使用 S.object().prop(...) 链式写法,枚举、格式约束(如 S.string().format('email'))全部声明在 fluent 层,常量(枚举值等)从外部变量注入,便于从数据库或配置中心取值;
  • bodyparamsquery(或 querystring)、headersresponse.<状态码> 五个位置都可以直接放 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 响应体的逐项断言,便于编写端到端测试。
登录后查看全文
热门项目推荐
相关项目推荐