验证和序列化
Fastify 使用基于模式的方法。我们推荐使用 JSON Schema 来验证路由并序列化输出。 Fastify 将模式编译为高性能函数。
仅在内容类型为 application/json
时才会尝试进行验证。
所有示例都使用了 JSON Schema Draft 7 规范。
⚠ 警告: 将模式定义视为应用程序代码。验证和序列化功能使用
new Function()
,这在处理用户提供的模式时是不安全的。详情请参阅 Ajv 和 fast-json-stringify 。虽然 Fastify 支持
$async
Ajv 功能,但不应将其用于初始验证。在验证过程中访问数据库可能导致拒绝服务攻击。使用 Fastify 的钩子 如preHandler
来处理验证后的异步任务。
核心概念
验证和序列化由两个可自定义的依赖项处理:
- Ajv v8 用于请求验证
- fast-json-stringify 用于响应体序列化
这些依赖项仅共享通过 .addSchema(schema)
添加到 Fastify 实例中的 JSON 模式。
添加共享模式
addSchema
API 允许向 Fastify 实例添加多个模式,以便在整个应用程序中重用。此 API 是封装的。
共享模式可以使用 JSON Schema $ref
关键字进行重用。以下是引用工作方式的概述:
myField: { $ref: '#foo' }
在当前模式中搜索$id: '#foo'
myField: { $ref: '#/definitions/foo' }
在当前模式中搜索definitions.foo
myField: { $ref: 'http://url.com/sh.json#' }
搜索具有$id: 'http://url.com/sh.json'
的共享模式myField: { $ref: 'http://url.com/sh.json#/definitions/foo' }
搜索具有$id: 'http://url.com/sh.json'
的共享模式,并使用definitions.foo
myField: { $ref: 'http://url.com/sh.json#foo' }
搜索具有$id: 'http://url.com/sh.json'
的共享模式,在其中查找$id: '#foo'
简单用法:
fastify.addSchema({
$id: 'http://example.com/',
type: 'object',
properties: {
hello: { type: 'string' }
}
})
fastify.post('/', {
handler () {},
schema: {
body: {
type: 'array',
items: { $ref: 'http://example.com#/properties/hello' }
}
}
})
$ref
作为根引用:
fastify.addSchema({
$id: 'commonSchema',
type: 'object',
properties: {
hello: { type: 'string' }
}
})
fastify.post('/', {
handler () {},
schema: {
body: { $ref: 'commonSchema#' },
headers: { $ref: 'commonSchema#' }
}
})
获取共享模式
如果验证器和序列化器被自定义,.addSchema
将不再有用,因为 Fastify 不再控制它们。要访问添加到 Fastify 实例的模式,请使用 .getSchemas()
:
fastify.addSchema({
$id: 'schemaId',
type: 'object',
properties: {
hello: { type: 'string' }
}
})
const mySchemas = fastify.getSchemas()
const mySchema = fastify.getSchema('schemaId')
getSchemas
函数封装了返回选定范围内的共享模式:
fastify.addSchema({ $id: 'one', my: 'hello' })
// 只会返回 `one` 模式
fastify.get('/', (request, reply) => { reply.send(fastify.getSchemas()) })
fastify.register((instance, opts, done) => {
instance.addSchema({ $id: 'two', my: 'ciao' })
// 返回 `one` 和 `two` 模式
instance.get('/sub', (request, reply) => { reply.send(instance.getSchemas()) })
instance.register((subinstance, opts, done) => {
subinstance.addSchema({ $id: 'three', my: 'hola' })
// 返回 `one`, `two` 和 `three`
subinstance.get('/deep', (request, reply) => { reply.send(subinstance.getSchemas()) })
done()
})
done()
})
验证
路由验证依赖于 Ajv v8 ,这是一个高性能的 JSON Schema 验证器。为了验证输入,需要将所需的字段添加到路由模式中。
支持的验证包括:
body
:对 POST、PUT 或 PATCH 方法的请求体进行验证。querystring
或query
:对查询字符串进行验证。params
:对路由参数进行验证。headers
:对请求头进行验证。
验证可以是一个包含 type: 'object'
和 'properties'
对象的完整 JSON Schema 对象,也可以是顶层列出参数的简化版本。
ℹ 使用最新版 Ajv(v8)请参阅
schemaController
部分。
示例:
const bodyJsonSchema = {
type: 'object',
required: ['requiredKey'],
properties: {
someKey: { type: 'string' },
someOtherKey: { type: 'number' },
requiredKey: {
type: 'array',
maxItems: 3,
items: { type: 'integer' }
},
nullableKey: { type: ['number', 'null'] }, // 或者 { type: 'number', nullable: true }
multipleTypesKey: { type: ['boolean', 'number'] },
multipleRestrictedTypesKey: {
oneOf: [
{ type: 'string', maxLength: 5 },
{ type: 'number', minimum: 10 }
]
},
enumKey: {
type: 'string',
enum: ['John', 'Foo']
},
notTypeKey: {
not: { type: 'array' }
}
}
}
const queryStringJsonSchema = {
type: 'object',
properties: {
name: { type: 'string' },
excitement: { type: 'integer' }
}
}
const paramsJsonSchema = {
type: 'object',
properties: {
par1: { type: 'string' },
par2: { type: 'number' }
}
}
const headersJsonSchema = {
type: 'object',
properties: {
'x-foo': { type: 'string' }
},
required: ['x-foo']
}
const schema = {
body: bodyJsonSchema,
querystring: queryStringJsonSchema,
params: paramsJsonSchema,
headers: headersJsonSchema
}
const schema = {
body: bodyJsonSchema,
querystring: queryStringJsonSchema,
params: paramsJsonSchema,
headers: headersJsonSchema
}
fastify.post('/the/url', { schema }, handler)
对于 body
的模式,还可以通过在 content
属性内嵌套模式来根据内容类型区分模式。模式验证将基于请求中的 Content-Type
头进行。
fastify.post('/the/url', {
schema: {
body: {
content: {
'application/json': {
schema: { type: 'object' }
},
'text/plain': {
schema: { type: 'string' }
}
// 其他内容类型将不会被验证
}
}
}
}, handler)
请注意,Ajv 将尝试根据模式中的 type
关键字对值进行 强制转换 ,以通过验证并使用正确类型的后续数据。
Fastify 中的 Ajv 默认配置支持将查询字符串中的数组参数进行强制转换。示例:
const opts = {
schema: {
querystring: {
type: 'object',
properties: {
ids: {
type: 'array',
default: []
},
},
}
}
}
fastify.get('/', opts, (request, reply) => {
reply.send({ params: request.query }) // 回显查询字符串
})
fastify.listen({ port: 3000 }, (err) => {
if (err) throw err
})
curl -X GET "http://localhost:3000/?ids=1
{"params":{"ids":["1"]}}
curl -X GET "http://localhost:3000/?ids=1
{"params":{"ids":["1"]}}
可以为每种参数类型(body、querystring、params、headers)指定自定义的模式验证器。
例如,以下代码仅禁用 body
参数的类型转换,并更改 Ajv 的默认选项:
const schemaCompilers = {
body: new Ajv({
removeAdditional: false,
coerceTypes: false,
allErrors: true
}),
params: new Ajv({
removeAdditional: false,
coerceTypes: true,
allErrors: true
}),
querystring: new Ajv({
removeAdditional: false,
coerceTypes: true,
allErrors: true
}),
headers: new Ajv({
removeAdditional: false,
coerceTypes: true,
allErrors: true
})
}
server.setValidatorCompiler(req => {
if (!req.httpPart) {
throw new Error('缺少 httpPart')
}
const compiler = schemaCompilers[req.httpPart]
if (!compiler) {
throw new Error(`缺少 ${req.httpPart} 的编译器`)
}
return compiler.compile(req.schema)
})
有关更多信息,请参阅 Ajv Coercion 。
Ajv 插件
可以提供一个插件列表,用于与默认的 ajv
实例一起使用。
请确保插件与 Fastify 内部提供的 Ajv 版本兼容。
有关插件格式,请参阅
ajv options
。
const fastify = require('fastify')({
ajv: {
plugins: [
require('ajv-merge-patch')
]
}
})
fastify.post('/', {
handler (req, reply) { reply.send({ ok: 1 }) },
schema: {
body: {
$patch: {
source: {
type: 'object',
properties: {
q: {
type: 'string'
}
}
},
with: [
{
op: 'add',
path: '/properties/q',
value: { type: 'number' }
}
]
}
}
}
})
fastify.post('/foo', {
handler (req, reply) { reply.send({ ok: 1 }) },
schema: {
body: {
$merge: {
source: {
type: 'object',
properties: {
q: {
type: 'string'
}
}
},
with: {
required: ['q']
}
}
}
}
})
验证器编译器
validatorCompiler
是一个返回用于验证请求体、URL 参数、头部和查询字符串的函数。默认的 validatorCompiler
返回一个实现 ajv 验证接口的函数。Fastify 内部使用它来加速验证过程。
Fastify 的 基础 ajv 配置 是:
{
coerceTypes: 'array', // 将数据类型转换为与 type 关键字匹配的类型
useDefaults: true, // 使用相应的 default 关键字值替换缺失的属性和项
removeAdditional: true, // 如果 additionalProperties 设置为 false,则移除额外的属性,详情请见:https://ajv.js.org/guide/modifying-data.html#removing-additional-properties
uriResolver: require('fast-uri'),
addUsedSchema: false,
// 显式设置 allErrors 为 `false`。
// 当设置为 `true` 时,可能会导致 DoS 攻击。
allErrors: false
}
通过提供 ajv.customOptions
来修改基础配置。
要更改或添加其他配置选项,请创建自定义实例并覆盖现有实例:
const fastify = require('fastify')()
const Ajv = require('ajv')
const ajv = new Ajv({
removeAdditional: 'all',
useDefaults: true,
coerceTypes: 'array',
// 其他选项
// ...
})
fastify.setValidatorCompiler(({ schema, method, url, httpPart }) => {
return ajv.compile(schema)
})
ℹ️ 注意:当使用自定义验证器实例时,将模式添加到验证器而不是 Fastify 中。Fastify 的
addSchema
方法不会识别自定义验证器。
使用其他验证库
setValidatorCompiler
函数允许用其他 JavaScript 验证库(如 joi 或 yup )或自定义的验证库替换 ajv
:
const Joi = require('joi')
fastify.post('/the/url', {
schema: {
body: Joi.object().keys({
hello: Joi.string().required()
}).required()
},
validatorCompiler: ({ schema, method, url, httpPart }) => {
return data => schema.validate(data)
}
}, handler)
const yup = require('yup')
// 验证选项以匹配 Fastify 中使用的 ajv 基线选项
const yupOptions = {
strict: false,
abortEarly: false, // 返回所有错误
stripUnknown: true, // 移除额外属性
recursive: true
}
fastify.post('/the/url', {
schema: {
body: yup.object({
age: yup.number().integer().required(),
sub: yup.object().shape({
name: yup.string().required()
}).required()
})
},
validatorCompiler: ({ schema, method, url, httpPart }) => {
return function (data) {
// 当 strict = false 时,yup 的 `validateSync` 函数在验证成功时返回转换后的值,在验证失败时抛出异常
try {
const result = schema.validateSync(data, yupOptions)
return { value: result }
} catch (e) {
return { error: e }
}
}
}
}, handler)
.statusCode 属性
所有验证错误都具有一个 .statusCode
属性,该属性被设置为 400
,确保默认的错误处理程序将响应状态码设置为 400
。
fastify.setErrorHandler(function (error, request, reply) {
request.log.error(error, `此错误的状态码是 ${error.statusCode}`)
reply.status(error.statusCode).send(error)
})
使用其他验证库的验证消息
Fastify 的验证错误消息与其默认验证引擎紧密耦合:ajv
返回的错误最终会通过 schemaErrorFormatter
函数处理,该函数构建了易于人类理解的错误消息。然而,schemaErrorFormatter
函数是为 ajv
设计的,在使用其他验证库时可能会导致奇怪或不完整的错误消息。
为了避免这个问题,有以下两种主要选项:
- 确保自定义
schemaCompiler
返回的验证函数返回与ajv
结构和格式相同的错误。 - 使用自定义
errorHandler
拦截并格式化自定义验证错误。
Fastify 向所有验证错误添加了两个属性,以帮助编写自定义 errorHandler
:
validation
: 验证函数(由自定义schemaCompiler
返回)返回的对象的error
属性的内容validationContext
: 验证错误发生的上下文(body、params、query 或 headers)
以下是一个处理验证错误的自定义 errorHandler
示例:
const errorHandler = (error, request, reply) => {
const statusCode = error.statusCode
let response
const { validation, validationContext } = error
// 检查是否为验证错误
if (validation) {
response = {
// validationContext 将是 'body'、'params'、'headers' 或 'query'
message: `在验证 ${validationContext} 时发生验证错误...`,
// 这是由验证库返回的结果...
errors: validation
}
} else {
response = {
message: '发生错误...'
}
}
// 在这里进行其他操作,例如记录错误
// ...
reply.status(statusCode).send(response)
}
序列化
Fastify 使用 fast-json-stringify 将数据作为 JSON 发送,如果在路由选项中提供了输出模式。使用输出模式可以显著提高吞吐量,并有助于防止敏感信息的意外泄露。
示例:
const schema = {
response: {
200: {
type: 'object',
properties: {
value: { type: 'string' },
otherValue: { type: 'boolean' }
}
}
}
}
fastify.post('/the/url', { schema }, handler)
响应模式基于状态码。为了在多个状态码上使用相同的模式,可以使用 '2xx'
或 default
,例如:
const schema = {
response: {
default: {
type: 'object',
properties: {
error: {
type: 'boolean',
default: true
}
}
},
'2xx': {
type: 'object',
properties: {
value: { type: 'string' },
otherValue: { type: 'boolean' }
}
},
201: {
// 合约语法
value: { type: 'string' }
}
}
}
fastify.post('/the/url', { schema }, handler)
可以为不同的内容类型定义特定的响应模式。 例如:
const schema = {
response: {
200: {
description: '支持不同内容类型的响应模式',
content: {
'application/json': {
schema: {
name: { type: 'string' },
image: { type: 'string' },
address: { type: 'string' }
}
},
'application/vnd.v1+json': {
schema: {
type: 'array',
items: { $ref: 'test' }
}
}
}
},
'3xx': {
content: {
'application/vnd.v2+json': {
schema: {
fullName: { type: 'string' },
phone: { type: 'string' }
}
}
}
},
default: {
content: {
// */* 是匹配所有内容类型
'*/*': {
schema: {
desc: { type: 'string' }
}
}
}
}
}
}
fastify.post('/url', { schema }, handler)
序列化编译器
serializerCompiler
返回一个函数,该函数必须从输入对象返回一个字符串。在定义响应 JSON Schema 时,通过提供用于序列化的每个路由的函数来更改默认序列化方法。
fastify.setSerializerCompiler(({ schema, method, url, httpStatus, contentType }) => {
return data => JSON.stringify(data)
})
fastify.get('/user', {
handler (req, reply) {
reply.send({ id: 1, name: 'Foo', image: 'BIG IMAGE' })
},
schema: {
response: {
'2xx': {
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' }
}
}
}
}
})
要在代码的特定部分设置自定义序列化器,请使用
reply.serializer(...)
错误处理
当请求的模式验证失败时,Fastify 将自动返回一个包含验证结果的状态码为 400 的响应。例如,如果使用以下模式定义路由:
const schema = {
body: {
type: 'object',
properties: {
name: { type: 'string' }
},
required: ['name']
}
}
如果请求未能满足该模式,则路由将返回包含以下负载的响应:
{
"statusCode": 400,
"error": "Bad Request",
"message": "body should have required property 'name'"
}
要在路由内部处理错误,请指定 attachValidation
选项。如果有验证错误,请求对象中的 validationError
属性将包含原始验证结果的 Error
对象,如下所示:
const fastify = Fastify()
fastify.post('/', { schema, attachValidation: true }, function (req, reply) {
if (req.validationError) {
// `req.validationError.validation` 包含原始验证错误
reply.code(400).send(req.validationError)
}
})
schemaErrorFormatter
为了格式化错误,可以在实例化 Fastify 时提供一个同步函数作为 schemaErrorFormatter
选项。该上下文函数将是 Fastify 服务器实例。
errors
是一个包含 Fastify 架构验证错误(FastifySchemaValidationError
)的数组。dataVar
是当前正在验证的架构部分(如参数、请求体、查询字符串、头部信息)。
const fastify = Fastify({
schemaErrorFormatter: (errors, dataVar) => {
// ... 我的格式化逻辑
return new Error(myErrorMessage)
}
})
// 或者
fastify.setSchemaErrorFormatter(function (errors, dataVar) {
this.log.error({ err: errors }, '验证失败')
// ... 我的格式化逻辑
return new Error(myErrorMessage)
})
使用 setErrorHandler 来为验证错误定义自定义响应,例如:
fastify.setErrorHandler(function (error, request, reply) {
if (error.validation) {
reply.status(422).send(new Error('验证失败'))
}
})
对于模式中自定义错误响应,请参阅ajv-errors
。查看示例 的用法。
安装
ajv-errors
的版本 1.0.1,因为后续版本与 AJV v6(Fastify v3 配套的版本)不兼容。
下面是一个示例,展示了如何通过提供自定义 AJV 选项来为模式中的每个属性添加自定义错误消息。模式中的内联注释描述了如何配置以显示每种情况下的不同错误消息:
const fastify = Fastify({
ajv: {
customOptions: {
jsonPointers: true,
// ⚠ 警告:启用此选项可能会导致以下安全问题 https://www.cvedetails.com/cve/CVE-2020-8192/
allErrors: true
},
plugins: [
require('ajv-errors')
]
}
})
const schema = {
body: {
type: 'object',
properties: {
name: {
type: 'string',
errorMessage: {
type: 'Bad name'
}
},
age: {
type: 'number',
errorMessage: {
type: 'Bad age', // 指定自定义消息
min: 'Too young' // 除了 required 外的所有约束条件
}
}
},
required: ['name', 'age'],
errorMessage: {
required: {
name: 'Why no name!', // 当属性缺失时指定错误信息
age: 'Why no age!' // 属性缺失时的输入
}
}
}
}
fastify.post('/', { schema, }, (request, reply) => {
reply.send({
hello: 'world'
})
})
要返回本地化的错误消息,请参阅 ajv-i18n 。
const localize = require('ajv-i18n')
const fastify = Fastify()
const schema = {
body: {
type: 'object',
properties: {
name: {
type: 'string',
},
age: {
type: 'number',
}
},
required: ['name', 'age'],
}
}
fastify.setErrorHandler(function (error, request, reply) {
if (error.validation) {
localize.ru(error.validation)
reply.status(400).send(error.validation)
return
}
reply.send(error)
})
JSON Schema支持
JSON Schema提供了优化模式的工具。结合Fastify的共享模式,所有模式都可以轻松重用。
使用场景 | 验证器 | 序列化器 |
---|---|---|
$ref 到 $id | ️️✔️ | ✔️ |
$ref 到 /definitions | ✔️ | ✔️ |
$ref 到共享模式的 $id | ✔️ | ✔️ |
$ref 到共享模式的 /definitions | ✔️ | ✔️ |
示例
在同一 JSON Schema 中使用 $ref
指向 $id
const refToId = {
type: 'object',
definitions: {
foo: {
$id: '#address',
type: 'object',
properties: {
city: { type: 'string' }
}
}
},
properties: {
home: { $ref: '#address' },
work: { $ref: '#address' }
}
}
在同一 JSON Schema 中使用 $ref
指向 /definitions
const refToDefinitions = {
type: 'object',
definitions: {
foo: {
$id: '#address',
type: 'object',
properties: {
city: { type: 'string' }
}
}
},
properties: {
home: { $ref: '#/definitions/foo' },
work: { $ref: '#/definitions/foo' }
}
}
使用 $ref
引用外部模式中的共享模式 $id
fastify.addSchema({
$id: 'http://foo/common.json',
type: 'object',
definitions: {
foo: {
$id: '#address',
type: 'object',
properties: {
city: { type: 'string' }
}
}
}
})
const refToSharedSchemaId = {
type: 'object',
properties: {
home: { $ref: 'http://foo/common.json#address' },
work: { $ref: 'http://foo/common.json#address' }
}
}
使用 $ref
引用外部模式中的共享定义 /definitions
fastify.addSchema({
$id: 'http://foo/shared.json',
type: 'object',
definitions: {
foo: {
type: 'object',
properties: {
city: { type: 'string' }
}
}
}
})
const refToSharedSchemaDefinitions = {
type: 'object',
properties: {
home: { $ref: 'http://foo/shared.json#/definitions/foo' },
work: { $ref: 'http://foo/shared.json#/definitions/foo' }
}
}
资源
- JSON Schema
- 理解 JSON Schema
- fast-json-stringify 文档
- Ajv 文档
- Ajv i18n
- Ajv 自定义错误
- 使用核心方法和错误文件转储进行自定义错误处理的示例:example