验证和序列化
验证和序列化
Fastify 使用基于模式的方法,即使它不是强制性的,我们也建议使用 JSON Schema 来验证您的路由并序列化您的输出。在内部,Fastify 将模式编译成一个高性能函数。
如 内容类型解析器 文档中所述,只有当内容类型为 application-json
时,才会尝试进行验证。
本节中的所有示例都使用 JSON Schema 草案 7 规范。
⚠ 安全提示
将模式定义视为应用程序代码。验证和序列化功能使用
new Function()
动态评估代码,这对于使用用户提供的模式并不安全。有关更多详细信息,请参阅 Ajv 和 fast-json-stringify。尽管 Fastify 支持
$async
Ajv 功能,但它不应作为第一种验证策略的一部分使用。此选项用于访问数据库并在验证过程中读取它们可能会导致对应用程序的拒绝服务攻击。如果您需要运行async
任务,请在验证完成后改用 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' })
// will return only `one` schema
fastify.get('/', (request, reply) => { reply.send(fastify.getSchemas()) })
fastify.register((instance, opts, done) => {
instance.addSchema({ $id: 'two', my: 'ciao' })
// will return `one` and `two` schemas
instance.get('/sub', (request, reply) => { reply.send(instance.getSchemas()) })
instance.register((subinstance, opts, done) => {
subinstance.addSchema({ $id: 'three', my: 'hola' })
// will return `one`, `two` and `three`
subinstance.get('/deep', (request, reply) => { reply.send(subinstance.getSchemas()) })
done()
})
done()
})
验证
路由验证在内部依赖于 Ajv v8,它是一个高性能的 JSON Schema 验证器。验证输入非常容易:只需在路由模式中添加所需的字段,就完成了!
支持的验证包括:
body
:如果请求方法为 POST、PUT 或 PATCH,则验证请求的主体。querystring
或query
:验证查询字符串。params
:验证路由参数。headers
:验证请求头。
所有验证都可以是完整的 JSON Schema 对象(具有 type
属性为 'object'
和 'properties'
对象包含参数),或者更简单的变体,其中 type
和 properties
属性被省略,并且参数列在顶层(请参见下面的示例)。
ℹ 如果您需要使用 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'] }, // or { 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
}
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' }
}
// Other content types will not be validated
}
}
}
}, handler)
请注意,Ajv 会尝试 强制转换 值到您的模式 type
关键字中指定的类型,以通过验证并在之后使用正确类型的数据。
Fastify 中的 Ajv 默认配置支持在 querystring
中强制转换数组参数。示例
const opts = {
schema: {
querystring: {
type: 'object',
properties: {
ids: {
type: 'array',
default: []
},
},
}
}
}
fastify.get('/', opts, (request, reply) => {
reply.send({ params: request.query }) // echo the querystring
})
fastify.listen({ port: 3000 }, (err) => {
if (err) throw err
})
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('Missing httpPart')
}
const compiler = schemaCompilers[req.httpPart]
if (!compiler) {
throw new Error(`Missing compiler for ${req.httpPart}`)
}
return compiler.compile(req.schema)
})
有关更多信息,请参阅 此处
Ajv 插件
您可以提供一个插件列表,您希望将这些插件与默认的 ajv
实例一起使用。请注意,插件必须与 **Fastify 中提供的 Ajv 版本兼容**。
请参阅
ajv 选项
以检查插件格式
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', // change data type of data to match type keyword
useDefaults: true, // replace missing properties and items with the values from corresponding default keyword
removeAdditional: true, // remove additional properties if additionalProperties is set to false, see: https://ajv.js.org/guide/modifying-data.html#removing-additional-properties
uriResolver: require('fast-uri'),
addUsedSchema: false,
// Explicitly set allErrors to `false`.
// When set to `true`, a DoS attack is possible.
allErrors: false
}
可以通过向您的 Fastify 工厂提供 ajv.customOptions
来修改此基线配置。
如果您想更改或设置其他配置选项,则需要创建您自己的实例并覆盖现有的实例,如下所示:
const fastify = require('fastify')()
const Ajv = require('ajv')
const ajv = new Ajv({
removeAdditional: 'all',
useDefaults: true,
coerceTypes: 'array',
// any other options
// ...
})
fastify.setValidatorCompiler(({ schema, method, url, httpPart }) => {
return ajv.compile(schema)
})
注意:如果您使用任何验证器的自定义实例(即使是 Ajv),您都必须将模式添加到验证器而不是 Fastify,因为 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')
// Validation options to match ajv's baseline options used in Fastify
const yupOptions = {
strict: false,
abortEarly: false, // return all errors
stripUnknown: true, // remove additional properties
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) {
// with option strict = false, yup `validateSync` function returns the
// coerced value if validation was successful, or throws if validation failed
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, `This error has status code ${error.statusCode}`)
reply.status(error.statusCode).send(error)
})
使用其他验证库的验证消息
Fastify 的验证错误消息与默认验证引擎紧密耦合:从 ajv
返回的错误最终会通过 schemaErrorFormatter
函数运行,该函数负责构建用户友好的错误消息。但是,schemaErrorFormatter
函数是在 ajv
的基础上编写的。因此,在使用其他验证库时,您可能会遇到奇怪或不完整的错误消息。
要解决此问题,您有两个主要选项:
- 确保您的验证函数(由您的自定义
schemaCompiler
返回)返回与ajv
相同结构和格式的错误(尽管由于验证引擎之间的差异,这可能被证明是困难和棘手的) - 或使用自定义
errorHandler
来拦截和格式化您的“自定义”验证错误
为了帮助您编写自定义 errorHandler
,Fastify 向所有验证错误添加了两个属性:
validation
:验证函数(由您的自定义schemaCompiler
返回)返回的对象的error
属性的内容validationContext
:发生验证错误的“上下文”(body、params、query、headers)
下面显示了一个处理验证错误的自定义 errorHandler
的非常人为的示例:
const errorHandler = (error, request, reply) => {
const statusCode = error.statusCode
let response
const { validation, validationContext } = error
// check if we have a validation error
if (validation) {
response = {
// validationContext will be 'body' or 'params' or 'headers' or 'query'
message: `A validation error occurred when validating the ${validationContext}...`,
// this is the result of your validation library...
errors: validation
}
} else {
response = {
message: 'An error occurred...'
}
}
// any additional work here, eg. log error
// ...
reply.status(statusCode).send(response)
}
序列化
通常,您会将数据作为 JSON 发送给客户端,Fastify 有一个强大的工具来帮助您,fast-json-stringify,如果您在路由选项中提供了输出模式,则会使用此工具。我们鼓励您使用输出模式,因为它可以大大提高吞吐量并帮助防止意外泄露敏感信息。
示例
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: {
// the contract syntax
value: { type: 'string' }
}
}
}
fastify.post('/the/url', { schema }, handler)
您甚至可以为不同的内容类型提供特定的响应模式。例如
const schema = {
response: {
200: {
description: 'Response schema that support different content types'
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: {
// */* is match-all content-type
'*/*': {
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(...)
设置一个。
错误处理
当请求的 Schema 验证失败时,Fastify 将自动返回状态为 400 的响应,其中包含验证器结果作为有效负载。例如,如果您的路由具有以下 Schema
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
对象,其中包含如下所示的原始 validation
结果
const fastify = Fastify()
fastify.post('/', { schema, attachValidation: true }, function (req, reply) {
if (req.validationError) {
// `req.validationError.validation` contains the raw validation error
reply.code(400).send(req.validationError)
}
})
schemaErrorFormatter
如果您想自己格式化错误,可以在实例化时将一个必须返回错误的同步函数作为 schemaErrorFormatter
选项提供给 Fastify。上下文函数将是 Fastify 服务器实例。
errors
是 Fastify Schema 错误 FastifySchemaValidationError
的数组。dataVar
是 Schema 当前正在验证的部分。(params | body | querystring | headers)。
const fastify = Fastify({
schemaErrorFormatter: (errors, dataVar) => {
// ... my formatting logic
return new Error(myErrorMessage)
}
})
// or
fastify.setSchemaErrorFormatter(function (errors, dataVar) {
this.log.error({ err: errors }, 'Validation failed')
// ... my formatting logic
return new Error(myErrorMessage)
})
您还可以使用 setErrorHandler 为验证错误定义自定义响应,例如
fastify.setErrorHandler(function (error, request, reply) {
if (error.validation) {
reply.status(422).send(new Error('validation failed'))
}
})
如果您希望在 Schema 中快速轻松地获得自定义错误响应,请查看 ajv-errors
。查看 示例 用法。
请确保安装
ajv-errors
的 1.0.1 版本,因为更高版本与 AJV v6(Fastify v3 附带的版本)不兼容。
以下是一个示例,展示了如何通过提供自定义 AJV 选项为 Schema 的每个属性添加**自定义错误消息**。以下 Schema 中的内联注释描述了如何配置它以针对每种情况显示不同的错误消息
const fastify = Fastify({
ajv: {
customOptions: {
jsonPointers: true,
// Warning: Enabling this option may lead to this security issue 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', // specify custom message for
min: 'Too young' // all constraints except required
}
}
},
required: ['name', 'age'],
errorMessage: {
required: {
name: 'Why no name!', // specify error message for when the
age: 'Why no age!' // property is missing from input
}
}
}
}
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 提供了一些实用程序来优化您的 Schema,这些实用程序与 Fastify 的共享 Schema 结合使用,可以让您轻松地重用所有 Schema。
用例 | 验证器 | 序列化器 |
---|---|---|
$ref 到 $id | ️️✔️ | ✔️ |
$ref 到 /definitions | ✔️ | ✔️ |
$ref 到共享 Schema $id | ✔️ | ✔️ |
$ref 到共享 Schema /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
到共享 Schema $id
作为外部 Schema
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
到共享 Schema /definitions
作为外部 Schema
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 自定义错误
- 使用核心方法和错误文件转储进行自定义错误处理 示例