Validation-and-Serialization

Validation and Serialization

Fastify uses a schema-based approach, and even if it is not mandatory we recommend using JSON Schema to validate your routes and serialize your outputs. Internally, Fastify compiles the schema into a highly performant function.

Validation will only be attempted if the content type is application-json, as described in the documentation for the content type parser.

⚠ Security Notice

Treat the schema definition as application code. Validation and serialization features dynamically evaluate code with new Function(), which is not safe to use with user-provided schemas. See Ajv and fast-json-stringify for more details.

Moreover, the $async Ajv feature should not be used as part of the first validation strategy. This option is used to access Databases and reading them during the validation process may lead to Denial of Service Attacks to your application. If you need to run async tasks, use Fastify’s hooks instead after validation completes, such as preHandler.

Core concepts

The validation and the serialization tasks are processed by two different, and customizable, actors:

These two separate entities share only the JSON schemas added to Fastify’s instance through .addSchema(schema).

Adding a shared schema

Thanks to the addSchema API, you can add multiple schemas to the Fastify instance and then reuse them in multiple parts of your application. As usual, this API is encapsulated.

The shared schemas can be reused through the JSON Schema $ref keyword. Here an overview of how references work:

  • myField: { $ref: '#foo'} will search for field with $id: '#foo' inside the current schema
  • myField: { $ref: '#/definitions/foo'} will search for field definitions.foo inside the current schema
  • myField: { $ref: 'http://url.com/sh.json#'} will search for a shared schema added with $id: 'http://url.com/sh.json'
  • myField: { $ref: 'http://url.com/sh.json#/definitions/foo'} will search for a shared schema added with $id: 'http://url.com/sh.json' and will use the field definitions.foo
  • myField: { $ref: 'http://url.com/sh.json#foo'} will search for a shared schema added with $id: 'http://url.com/sh.json' and it will look inside of it for object with $id: '#foo'

Simple usage:

  1. fastify.addSchema({
  2. $id: 'http://example.com/',
  3. type: 'object',
  4. properties: {
  5. hello: { type: 'string' }
  6. }
  7. })
  8. fastify.post('/', {
  9. handler () {},
  10. schema: {
  11. body: {
  12. type: 'array',
  13. items: { $ref: 'http://example.com#/properties/hello' }
  14. }
  15. }
  16. })

$ref as root reference:

  1. fastify.addSchema({
  2. $id: 'commonSchema',
  3. type: 'object',
  4. properties: {
  5. hello: { type: 'string' }
  6. }
  7. })
  8. fastify.post('/', {
  9. handler () {},
  10. schema: {
  11. body: { $ref: 'commonSchema#' },
  12. headers: { $ref: 'commonSchema#' }
  13. }
  14. })

Retrieving the shared schemas

If the validator and the serializer are customized, the .addSchema method will not be useful since the actors are no longer controlled by Fastify. To access the schemas added to the Fastify instance, you can simply use .getSchemas():

  1. fastify.addSchema({
  2. $id: 'schemaId',
  3. type: 'object',
  4. properties: {
  5. hello: { type: 'string' }
  6. }
  7. })
  8. const mySchemas = fastify.getSchemas()
  9. const mySchema = fastify.getSchema('schemaId')

As usual, the function getSchemas is encapsulated and returns the shared schemas available in the selected scope:

  1. fastify.addSchema({ $id: 'one', my: 'hello' })
  2. // will return only `one` schema
  3. fastify.get('/', (request, reply) => { reply.send(fastify.getSchemas()) })
  4. fastify.register((instance, opts, done) => {
  5. instance.addSchema({ $id: 'two', my: 'ciao' })
  6. // will return `one` and `two` schemas
  7. instance.get('/sub', (request, reply) => { reply.send(instance.getSchemas()) })
  8. instance.register((subinstance, opts, done) => {
  9. subinstance.addSchema({ $id: 'three', my: 'hola' })
  10. // will return `one`, `two` and `three`
  11. subinstance.get('/deep', (request, reply) => { reply.send(subinstance.getSchemas()) })
  12. done()
  13. })
  14. done()
  15. })

Validation

The route validation internally relies upon Ajv v6 which is a high-performance JSON Schema validator. Validating the input is very easy: just add the fields that you need inside the route schema, and you are done!

The supported validations are:

  • body: validates the body of the request if it is a POST, PUT, or PATCH method.
  • querystring or query: validates the query string.
  • params: validates the route params.
  • headers: validates the request headers.

All the validations can be a complete JSON Schema object (with a type property of 'object' and a 'properties' object containing parameters) or a simpler variation in which the type and properties attributes are forgone and the parameters are listed at the top level (see the example below).

ℹ If you need to use the lastest version of Ajv (v8) you should read how to do it in the schemaController section. It is explained the easier way to avoid to implement a custom validator.

Example:

  1. const bodyJsonSchema = {
  2. type: 'object',
  3. required: ['requiredKey'],
  4. properties: {
  5. someKey: { type: 'string' },
  6. someOtherKey: { type: 'number' },
  7. requiredKey: {
  8. type: 'array',
  9. maxItems: 3,
  10. items: { type: 'integer' }
  11. },
  12. nullableKey: { type: ['number', 'null'] }, // or { type: 'number', nullable: true }
  13. multipleTypesKey: { type: ['boolean', 'number'] },
  14. multipleRestrictedTypesKey: {
  15. oneOf: [
  16. { type: 'string', maxLength: 5 },
  17. { type: 'number', minimum: 10 }
  18. ]
  19. },
  20. enumKey: {
  21. type: 'string',
  22. enum: ['John', 'Foo']
  23. },
  24. notTypeKey: {
  25. not: { type: 'array' }
  26. }
  27. }
  28. }
  29. const queryStringJsonSchema = {
  30. type: 'object',
  31. properties: {
  32. name: { type: 'string' },
  33. excitement: { type: 'integer' }
  34. }
  35. }
  36. const paramsJsonSchema = {
  37. type: 'object',
  38. properties: {
  39. par1: { type: 'string' },
  40. par2: { type: 'number' }
  41. }
  42. }
  43. const headersJsonSchema = {
  44. type: 'object',
  45. properties: {
  46. 'x-foo': { type: 'string' }
  47. },
  48. required: ['x-foo']
  49. }
  50. const schema = {
  51. body: bodyJsonSchema,
  52. querystring: queryStringJsonSchema,
  53. params: paramsJsonSchema,
  54. headers: headersJsonSchema
  55. }
  56. fastify.post('/the/url', { schema }, handler)

Note that Ajv will try to coerce the values to the types specified in your schema type keywords, both to pass the validation and to use the correctly typed data afterwards.

The Ajv default configuration in Fastify doesn’t support coercing array parameters in querystring. However, Fastify allows customOptions in Ajv instance. The coerceTypes: 'array' will coerce one parameter to a single element in array. Example:

  1. const opts = {
  2. schema: {
  3. querystring: {
  4. type: 'object',
  5. properties: {
  6. ids: {
  7. type: 'array',
  8. default: []
  9. },
  10. },
  11. }
  12. }
  13. }
  14. fastify.get('/', opts, (request, reply) => {
  15. reply.send({ params: request.query })
  16. })
  17. fastify.listen(3000, (err) => {
  18. if (err) throw err
  19. })

Using Fastify defaults the following request will result in 400 status code:

  1. curl -X GET "http://localhost:3000/?ids=1
  2. {"statusCode":400,"error":"Bad Request","message":"querystring/hello should be array"}

Using coerceTypes as ‘array’ will fix it:

  1. const ajv = new Ajv({
  2. removeAdditional: true,
  3. useDefaults: true,
  4. coerceTypes: 'array', // This line
  5. allErrors: true
  6. })
  7. fastify.setValidatorCompiler(({ schema, method, url, httpPart }) => {
  8. return ajv.compile(schema)
  9. })
  1. curl -X GET "http://localhost:3000/?ids=1
  2. {"params":{"hello":["1"]}}

You can also specify a custom schema validator for each parameter type (body, querystring, params, headers).

For example, the following code disable type coercion only for the body parameters, changing the ajv default options:

  1. const schemaCompilers = {
  2. body: new Ajv({
  3. removeAdditional: false,
  4. coerceTypes: false,
  5. allErrors: true
  6. }),
  7. params: new Ajv({
  8. removeAdditional: false,
  9. coerceTypes: true,
  10. allErrors: true
  11. }),
  12. querystring: new Ajv({
  13. removeAdditional: false,
  14. coerceTypes: true,
  15. allErrors: true
  16. }),
  17. headers: new Ajv({
  18. removeAdditional: false,
  19. coerceTypes: true,
  20. allErrors: true
  21. })
  22. }
  23. server.setValidatorCompiler(req => {
  24. if (!req.httpPart) {
  25. throw new Error('Missing httpPart')
  26. }
  27. const compiler = schemaCompilers[req.httpPart]
  28. if (!compiler) {
  29. throw new Error(`Missing compiler for ${req.httpPart}`)
  30. }
  31. return compiler.compile(req.schema)
  32. })

For further information see here

Ajv Plugins

You can provide a list of plugins you want to use with the default ajv instance. Note that the plugin must be compatible with Ajv v6.

Refer to ajv options to check plugins format

  1. const fastify = require('fastify')({
  2. ajv: {
  3. plugins: [
  4. require('ajv-merge-patch')
  5. ]
  6. }
  7. })
  8. fastify.post('/', {
  9. handler (req, reply) { reply.send({ ok: 1 }) },
  10. schema: {
  11. body: {
  12. $patch: {
  13. source: {
  14. type: 'object',
  15. properties: {
  16. q: {
  17. type: 'string'
  18. }
  19. }
  20. },
  21. with: [
  22. {
  23. op: 'add',
  24. path: '/properties/q',
  25. value: { type: 'number' }
  26. }
  27. ]
  28. }
  29. }
  30. }
  31. })
  32. fastify.post('/foo', {
  33. handler (req, reply) { reply.send({ ok: 1 }) },
  34. schema: {
  35. body: {
  36. $merge: {
  37. source: {
  38. type: 'object',
  39. properties: {
  40. q: {
  41. type: 'string'
  42. }
  43. }
  44. },
  45. with: {
  46. required: ['q']
  47. }
  48. }
  49. }
  50. }
  51. })

Validator Compiler

The validatorCompiler is a function that returns a function that validates 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 the validation up.

Fastify’s baseline ajv configuration is:

  1. {
  2. removeAdditional: true, // remove additional properties
  3. useDefaults: true, // replace missing properties and items with the values from corresponding default keyword
  4. coerceTypes: true, // change data type of data to match type keyword
  5. nullable: true // support keyword "nullable" from Open API 3 specification.
  6. }

This baseline configuration can be modified by providing ajv.customOptions to your Fastify factory.

If you want to change or set additional config options, you will need to create your own instance and override the existing one like:

  1. const fastify = require('fastify')()
  2. const Ajv = require('ajv')
  3. const ajv = new Ajv({
  4. // the fastify defaults (if needed)
  5. removeAdditional: true,
  6. useDefaults: true,
  7. coerceTypes: true,
  8. nullable: true,
  9. // any other options
  10. // ...
  11. })
  12. fastify.setValidatorCompiler(({ schema, method, url, httpPart }) => {
  13. return ajv.compile(schema)
  14. })

Note: If you use a custom instance of any validator (even Ajv), you have to add schemas to the validator instead of Fastify, since Fastify’s default validator is no longer used, and Fastify’s addSchema method has no idea what validator you are using.

Using other validation libraries

The setValidatorCompiler function makes it easy to substitute ajv with almost any Javascript validation library (joi, yup, …) or a custom one:

  1. const Joi = require('@hapi/joi')
  2. fastify.post('/the/url', {
  3. schema: {
  4. body: Joi.object().keys({
  5. hello: Joi.string().required()
  6. }).required()
  7. },
  8. validatorCompiler: ({ schema, method, url, httpPart }) => {
  9. return data => schema.validate(data)
  10. }
  11. }, handler)
  1. const yup = require('yup')
  2. // Validation options to match ajv's baseline options used in Fastify
  3. const yupOptions = {
  4. strict: false,
  5. abortEarly: false, // return all errors
  6. stripUnknown: true, // remove additional properties
  7. recursive: true
  8. }
  9. fastify.post('/the/url', {
  10. schema: {
  11. body: yup.object({
  12. age: yup.number().integer().required(),
  13. sub: yup.object().shape({
  14. name: yup.string().required()
  15. }).required()
  16. })
  17. },
  18. validatorCompiler: ({ schema, method, url, httpPart }) => {
  19. return function (data) {
  20. // with option strict = false, yup `validateSync` function returns the coerced value if validation was successful, or throws if validation failed
  21. try {
  22. const result = schema.validateSync(data, yupOptions)
  23. return { value: result }
  24. } catch (e) {
  25. return { error: e }
  26. }
  27. }
  28. }
  29. }, handler)
Validation messages with other validation libraries

Fastify’s validation error messages are tightly coupled to the default validation engine: errors returned from ajv are eventually run through the schemaErrorsText function which is responsible for building human-friendly error messages. However, the schemaErrorsText function is written with ajv in mind : as a result, you may run into odd or incomplete error messages when using other validation libraries.

To circumvent this issue, you have 2 main options :

  1. make sure your validation function (returned by your custom schemaCompiler) returns errors in the exact same structure and format as ajv (although this could prove to be difficult and tricky due to differences between validation engines)
  2. or use a custom errorHandler to intercept and format your ‘custom’ validation errors

To help you in writing a custom errorHandler, Fastify adds 2 properties to all validation errors:

  • validation: the content of the error property of the object returned by the validation function (returned by your custom schemaCompiler)
  • validationContext: the ‘context’ (body, params, query, headers) where the validation error occurred

A very contrived example of such a custom errorHandler handling validation errors is shown below:

  1. const errorHandler = (error, request, reply) => {
  2. const statusCode = error.statusCode
  3. let response
  4. const { validation, validationContext } = error
  5. // check if we have a validation error
  6. if (validation) {
  7. response = {
  8. // validationContext will be 'body' or 'params' or 'headers' or 'query'
  9. message: `A validation error occurred when validating the ${validationContext}...`,
  10. // this is the result of your validation library...
  11. errors: validation
  12. }
  13. } else {
  14. response = {
  15. message: 'An error occurred...'
  16. }
  17. }
  18. // any additional work here, eg. log error
  19. // ...
  20. reply.status(statusCode).send(response)
  21. }

Serialization

Usually, you will send your data to the clients as JSON, and Fastify has a powerful tool to help you, fast-json-stringify, which is used if you have provided an output schema in the route options. We encourage you to use an output schema, as it can drastically increase throughput and help prevent accidental disclosure of sensitive information.

Example:

  1. const schema = {
  2. response: {
  3. 200: {
  4. type: 'object',
  5. properties: {
  6. value: { type: 'string' },
  7. otherValue: { type: 'boolean' }
  8. }
  9. }
  10. }
  11. }
  12. fastify.post('/the/url', { schema }, handler)

As you can see, the response schema is based on the status code. If you want to use the same schema for multiple status codes, you can use '2xx', for example:

  1. const schema = {
  2. response: {
  3. '2xx': {
  4. type: 'object',
  5. properties: {
  6. value: { type: 'string' },
  7. otherValue: { type: 'boolean' }
  8. }
  9. },
  10. 201: {
  11. // the contract syntax
  12. value: { type: 'string' }
  13. }
  14. }
  15. }
  16. fastify.post('/the/url', { schema }, handler)

Serializer Compiler

The serializerCompiler is a function that returns a function that must return a string from an input object. When you define a response JSON Schema, you can change the default serialization method by providing a function to serialize every route where you do.

  1. fastify.setSerializerCompiler(({ schema, method, url, httpStatus }) => {
  2. return data => JSON.stringify(data)
  3. })
  4. fastify.get('/user', {
  5. handler (req, reply) {
  6. reply.send({ id: 1, name: 'Foo', image: 'BIG IMAGE' })
  7. },
  8. schema: {
  9. response: {
  10. '2xx': {
  11. id: { type: 'number' },
  12. name: { type: 'string' }
  13. }
  14. }
  15. }
  16. })

If you need a custom serializer in a very specific part of your code, you can set one with reply.serializer(…).

Error Handling

When schema validation fails for a request, Fastify will automatically return a status 400 response including the result from the validator in the payload. As an example, if you have the following schema for your route

  1. const schema = {
  2. body: {
  3. type: 'object',
  4. properties: {
  5. name: { type: 'string' }
  6. },
  7. required: ['name']
  8. }
  9. }

and fail to satisfy it, the route will immediately return a response with the following payload

  1. {
  2. "statusCode": 400,
  3. "error": "Bad Request",
  4. "message": "body should have required property 'name'"
  5. }

If you want to handle errors inside the route, you can specify the attachValidation option for your route. 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

  1. const fastify = Fastify()
  2. fastify.post('/', { schema, attachValidation: true }, function (req, reply) {
  3. if (req.validationError) {
  4. // `req.validationError.validation` contains the raw validation error
  5. reply.code(400).send(req.validationError)
  6. }
  7. })

schemaErrorFormatter

If you want to format errors yourself, you can provide a sync function that must return an error as the schemaErrorFormatter option to Fastify when instantiating. 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).

  1. const fastify = Fastify({
  2. schemaErrorFormatter: (errors, dataVar) => {
  3. // ... my formatting logic
  4. return new Error(myErrorMessage)
  5. }
  6. })
  7. // or
  8. fastify.setSchemaErrorFormatter(function (errors, dataVar) {
  9. this.log.error({ err: errors }, 'Validation failed')
  10. // ... my formatting logic
  11. return new Error(myErrorMessage)
  12. })

You can also use setErrorHandler to define a custom response for validation errors such as

  1. fastify.setErrorHandler(function (error, request, reply) {
  2. if (error.validation) {
  3. reply.status(422).send(new Error('validation failed'))
  4. }
  5. })

If you want custom error response in schema without headaches and quickly, you can take a look at ajv-errors. Check out the example usage.

Make sure to install version 1.0.1 of ajv-errors, because later versions of it 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 below describe how to configure it to show a different error message for each case:

  1. const fastify = Fastify({
  2. ajv: {
  3. customOptions: {
  4. jsonPointers: true,
  5. allErrors: true // Warning: Enabling this option may lead to this security issue https://www.cvedetails.com/cve/CVE-2020-8192/
  6. },
  7. plugins: [
  8. require('ajv-errors')
  9. ]
  10. }
  11. })
  12. const schema = {
  13. body: {
  14. type: 'object',
  15. properties: {
  16. name: {
  17. type: 'string',
  18. errorMessage: {
  19. type: 'Bad name'
  20. }
  21. },
  22. age: {
  23. type: 'number',
  24. errorMessage: {
  25. type: 'Bad age', // specify custom message for
  26. min: 'Too young' // all constraints except required
  27. }
  28. }
  29. },
  30. required: ['name', 'age'],
  31. errorMessage: {
  32. required: {
  33. name: 'Why no name!', // specify error message for when the
  34. age: 'Why no age!' // property is missing from input
  35. }
  36. }
  37. }
  38. }
  39. fastify.post('/', { schema, }, (request, reply) => {
  40. reply.send({
  41. hello: 'world'
  42. })
  43. })

If you want to return localized error messages, take a look at ajv-i18n

  1. const localize = require('ajv-i18n')
  2. const fastify = Fastify()
  3. const schema = {
  4. body: {
  5. type: 'object',
  6. properties: {
  7. name: {
  8. type: 'string',
  9. },
  10. age: {
  11. type: 'number',
  12. }
  13. },
  14. required: ['name', 'age'],
  15. }
  16. }
  17. fastify.setErrorHandler(function (error, request, reply) {
  18. if (error.validation) {
  19. localize.ru(error.validation)
  20. reply.status(400).send(error.validation)
  21. return
  22. }
  23. reply.send(error)
  24. })

JSON Schema support

JSON Schema has some type of utilities in order to optimize your schemas that, in conjunction with Fastify’s shared schema, let you reuse all your schemas easily.

Use CaseValidatorSerializer
$ref to $id️️✔️✔️
$ref to /definitions✔️✔️
$ref to shared schema $id✔️✔️
$ref to shared schema /definitions✔️✔️

Examples

Usage of $ref to $id in same JSON Schema
  1. const refToId = {
  2. type: 'object',
  3. definitions: {
  4. foo: {
  5. $id: '#address',
  6. type: 'object',
  7. properties: {
  8. city: { type: 'string' }
  9. }
  10. }
  11. },
  12. properties: {
  13. home: { $ref: '#address' },
  14. work: { $ref: '#address' }
  15. }
  16. }
Usage of $ref to /definitions in same JSON Schema
  1. const refToDefinitions = {
  2. type: 'object',
  3. definitions: {
  4. foo: {
  5. $id: '#address',
  6. type: 'object',
  7. properties: {
  8. city: { type: 'string' }
  9. }
  10. }
  11. },
  12. properties: {
  13. home: { $ref: '#/definitions/foo' },
  14. work: { $ref: '#/definitions/foo' }
  15. }
  16. }
Usage $ref to a shared schema $id as external schema
  1. fastify.addSchema({
  2. $id: 'http://foo/common.json',
  3. type: 'object',
  4. definitions: {
  5. foo: {
  6. $id: '#address',
  7. type: 'object',
  8. properties: {
  9. city: { type: 'string' }
  10. }
  11. }
  12. }
  13. })
  14. const refToSharedSchemaId = {
  15. type: 'object',
  16. properties: {
  17. home: { $ref: 'http://foo/common.json#address' },
  18. work: { $ref: 'http://foo/common.json#address' }
  19. }
  20. }
Usage $ref to a shared schema /definitions as external schema
  1. fastify.addSchema({
  2. $id: 'http://foo/shared.json',
  3. type: 'object',
  4. definitions: {
  5. foo: {
  6. type: 'object',
  7. properties: {
  8. city: { type: 'string' }
  9. }
  10. }
  11. }
  12. })
  13. const refToSharedSchemaDefinitions = {
  14. type: 'object',
  15. properties: {
  16. home: { $ref: 'http://foo/shared.json#/definitions/foo' },
  17. work: { $ref: 'http://foo/shared.json#/definitions/foo' }
  18. }
  19. }

Resources