Fastify uses a schema-based approach. We recommend using JSON Schema to validate routes and serialize outputs. Fastify compiles the schema into a highly performant function.
Validation is only attempted if the content type is application/json
.
All examples use the JSON Schema Draft 7 specification.
Treat schema definitions as application code. Validation and serialization features use
new Function()
, which is unsafe with user-provided schemas. See Ajv and fast-json-stringify for details.Whilst Fastify supports the
$async
Ajv feature, it should not be used for initial validation. Accessing databases during validation may lead to Denial of Service attacks. Use Fastify's hooks likepreHandler
forasync
tasks after validation.
Validation and serialization are handled by two customizable dependencies:
- Ajv v8 for request validation
- fast-json-stringify for response body serialization
These dependencies share only the JSON schemas added to Fastify's instance via
.addSchema(schema)
.
The addSchema
API allows adding multiple schemas to the Fastify instance for
reuse throughout the application. This API is encapsulated.
Shared schemas can be reused with the JSON Schema
$ref
keyword. Here is an overview of how references work:
myField: { $ref: '#foo' }
searches for$id: '#foo'
in the current schemamyField: { $ref: '#/definitions/foo' }
searches fordefinitions.foo
in the current schemamyField: { $ref: 'http://url.com/sh.json#' }
searches for a shared schema with$id: 'http://url.com/sh.json'
myField: { $ref: 'http://url.com/sh.json#/definitions/foo' }
searches for a shared schema with$id: 'http://url.com/sh.json'
and usesdefinitions.foo
myField: { $ref: 'http://url.com/sh.json#foo' }
searches for a shared schema with$id: 'http://url.com/sh.json'
and looks for$id: '#foo'
within it
Simple usage:
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
as root reference:
fastify.addSchema({
$id: 'commonSchema',
type: 'object',
properties: {
hello: { type: 'string' }
}
})
fastify.post('/', {
handler () {},
schema: {
body: { $ref: 'commonSchema#' },
headers: { $ref: 'commonSchema#' }
}
})
If the validator and serializer are customized, .addSchema
is not useful since
Fastify no longer controls them. To access schemas added to the Fastify instance,
use .getSchemas()
:
fastify.addSchema({
$id: 'schemaId',
type: 'object',
properties: {
hello: { type: 'string' }
}
})
const mySchemas = fastify.getSchemas()
const mySchema = fastify.getSchema('schemaId')
The getSchemas
function is encapsulated and returns shared schemas available
in the selected scope:
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()
})
Route validation relies on Ajv v8, a high-performance JSON Schema validator. To validate input, add the required fields to the route schema.
Supported validations include:
body
: validates the request body for POST, PUT, or PATCH methods.querystring
orquery
: validates the query string.params
: validates the route parameters.headers
: validates the request headers.
Validations can be a complete JSON Schema object with a type
of 'object'
and
a 'properties'
object containing parameters, or a simpler variation listing
parameters at the top level.
ℹ For using the latest Ajv (v8), refer to the
schemaController
section.
Example:
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)
For body
schema, it is further possible to differentiate the schema per content
type by nesting the schemas inside content
property. The schema validation
will be applied based on the Content-Type
header in the request.
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)
Note that Ajv will try to coerce values to
the types specified in the schema type
keywords, both to pass validation and
to use the correctly typed data afterwards.
The Ajv default configuration in Fastify supports coercing array parameters in
querystring
. Example:
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"]}}
A custom schema validator can be specified for each parameter type (body, querystring, params, headers).
For example, the following code disables type coercion only for the body
parameters, changing the Ajv default options:
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)
})
For more information, see Ajv Coercion.
A list of plugins can be provided for use with the default ajv
instance.
Ensure the plugin is compatible with the Ajv version shipped within Fastify.
Refer to
ajv options
to check plugins format.
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']
}
}
}
}
})
The validatorCompiler
is a function that returns a function to validate the
body, URL parameters, headers, and query string. The default validatorCompiler
returns a function that implements the ajv validation
interface. Fastify uses it internally to speed up validation.
Fastify's baseline ajv configuration is:
{
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
}
Modify the baseline configuration by providing
ajv.customOptions
to the Fastify factory.
To change or set additional config options, create a custom instance and override the existing one:
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)
})
Note: When using a custom validator instance, add schemas to the validator
instead of Fastify. Fastify's addSchema
method will not recognize the custom
validator.
The setValidatorCompiler
function allows substituting ajv
with other
JavaScript validation libraries like joi or
yup, or a custom one:
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)
All validation errors have a .statusCode
property set to 400
, ensuring the
default error handler sets the response status code to 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's validation error messages are tightly coupled to the default
validation engine: errors returned from ajv
are eventually run through the
schemaErrorFormatter
function which builds human-friendly error messages.
However, the schemaErrorFormatter
function is written with ajv
in mind.
This may result in odd or incomplete error messages when using other validation
libraries.
To circumvent this issue, there are two main options:
- Ensure the validation function (returned by the custom
schemaCompiler
) returns errors in the same structure and format asajv
. - Use a custom
errorHandler
to intercept and format custom validation errors.
Fastify adds two properties to all validation errors to help write a custom
errorHandler
:
validation
: the content of theerror
property of the object returned by the validation function (returned by the customschemaCompiler
)validationContext
: the context (body, params, query, headers) where the validation error occurred
A contrived example of such a custom errorHandler
handling validation errors
is shown below:
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', 'params', 'headers', or 'query'
message: `A validation error occurred when validating the ${validationContext}...`,
// this is the result of the validation library...
errors: validation
}
} else {
response = {
message: 'An error occurred...'
}
}
// any additional work here, eg. log error
// ...
reply.status(statusCode).send(response)
}
Fastify uses fast-json-stringify to send data as JSON if an output schema is provided in the route options. Using an output schema can drastically increase throughput and help prevent accidental disclosure of sensitive information.
Example:
const schema = {
response: {
200: {
type: 'object',
properties: {
value: { type: 'string' },
otherValue: { type: 'boolean' }
}
}
}
}
fastify.post('/the/url', { schema }, handler)
The response schema is based on the status code. To use the same schema for
multiple status codes, use '2xx'
or default
, for example:
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)
A specific response schema can be defined for different content types. For example:
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)
The serializerCompiler
returns a function that must return a string from an
input object. When defining a response JSON Schema, change the default
serialization method by providing a function to serialize each route.
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' }
}
}
}
}
})
To set a custom serializer in a specific part of the code, use
reply.serializer(...)
.
When schema validation fails for a request, Fastify will automatically return a status 400 response including the result from the validator in the payload. For example, if the following schema is used for a route:
const schema = {
body: {
type: 'object',
properties: {
name: { type: 'string' }
},
required: ['name']
}
}
If the request fails to satisfy the schema, the route will return a response with the following payload:
{
"statusCode": 400,
"error": "Bad Request",
"message": "body should have required property 'name'"
}
To handle errors inside the route, specify the attachValidation
option. If
there is a validation error, the validationError
property of the request will
contain the Error
object with the raw validation result as shown below:
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)
}
})
To format errors, provide a sync function that returns an error as the
schemaErrorFormatter
option when instantiating Fastify. The context function
will be the Fastify server instance.
errors
is an array of Fastify schema errors FastifySchemaValidationError
.
dataVar
is the currently validated part of the 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)
})
Use setErrorHandler to define a custom response for validation errors such as:
fastify.setErrorHandler(function (error, request, reply) {
if (error.validation) {
reply.status(422).send(new Error('validation failed'))
}
})
For custom error responses in the schema, see
ajv-errors
. Check out the
example
usage.
Install version 1.0.1 of
ajv-errors
, as later versions are not compatible with AJV v6 (the version shipped by Fastify v3).
Below is an example showing how to add custom error messages for each property of a schema by supplying custom AJV options. Inline comments in the schema describe how to configure it to show a different error message for each case:
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'
})
})
To return localized error messages, see 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 provides utilities to optimize schemas. Combined with Fastify's shared schema, all schemas can be easily reused.
Use Case | Validator | Serializer |
---|---|---|
$ref to $id |
️️✔️ | ✔️ |
$ref to /definitions |
✔️ | ✔️ |
$ref to shared schema $id |
✔️ | ✔️ |
$ref to shared schema /definitions |
✔️ | ✔️ |
const refToId = {
type: 'object',
definitions: {
foo: {
$id: '#address',
type: 'object',
properties: {
city: { type: 'string' }
}
}
},
properties: {
home: { $ref: '#address' },
work: { $ref: '#address' }
}
}
const refToDefinitions = {
type: 'object',
definitions: {
foo: {
$id: '#address',
type: 'object',
properties: {
city: { type: 'string' }
}
}
},
properties: {
home: { $ref: '#/definitions/foo' },
work: { $ref: '#/definitions/foo' }
}
}
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' }
}
}
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
- Understanding JSON Schema
- fast-json-stringify documentation
- Ajv documentation
- Ajv i18n
- Ajv custom errors
- Custom error handling with core methods with error file dumping example