loading...

Validation and Serialization in Fastify v3

eomm profile image Manuel Spigolon ・5 min read

This article will explain all you need to know to master the new validation and serialization with Fastify v3!
These components have been refactored and improved to give to devs more control, so let's explore them.

Architecture

Here it is the startup of a route with some validation and serialization configured:

startup architecture

It is not shown in the figure, but all this pipeline is fully encapsulated in the Fastify's style!

Validator Compiler

The validation is the process of strict validating the request's input, without async logic like DB access.
It will check if the request's parts are like you expect.

Those parts are:

  • headers
  • params (aka path parameters)
  • body
  • querystring (aka query parameters)

Since in Fastify all these domains are converted to JSON, it can be validated through the definition of a JSON Schema (draft-07)!
Fastify will use the Validator Compiler only in the startup phase to create a validation function starting from the Schemas provided to the route definition.
The validation function is attached to the route context, and then it will be executed for every new request when needed.

How to use it

The default Validator Compiler is ajv with this setup.
It can be customized thanks to the ajv option at the server declaration.

The default validator can be changed to use another validation module. There is the new setValidatorCompiler!

As showed in the image, this component will be called for every body, querystring, params and headers schemas defined in the route!
Now, with the new API interface, it is possible to understand what schema is going to be compiled:

fastify.setValidatorCompiler(function (schemaDefinition) {
  const { schema, method, url, httpPart } = schemaDefinition

  // schema: the JSON schema that has been settle on the route
  // method: the HTTP method of the route
  // url: the complete route's url
  // httpPart: it can be one of `body`, `querystring`, `params` and `headers`

  // all the JSON schemas added to the fastify's context can be read by:
  const schemas = fastify.getSchemas()

  // it is necessary to return a function, that will be called for every request!
  return function validateHttpThePart (data) {
    // data is a JSON that represents the incoming request's `httpPart`

    // this is a sync function that must return:
    // in case of success
    return { value: { this: 'will be the `httpPart` object in the handler' } }

    // or in case of error
    return { error: { this: 'will be a 400' } }
  }
})

There are many examples with other validators like Joi or yup in the official documentation.

Serializer Compiler

The serialization is the process of transforming an object to a byte stream to pipe in the response.
In our case, we will serialize the JSONs in strings to reply to the client's requests.
The main advantages of creating a response schema are:

  • performance: speed up the serialization process
  • security: you are not returning data that you don't want to
  • documentation: generation of OpenAPI docs site

Note that the serialization will not apply any validation to the data returned to the clients.
It will provide:

  • format definition of the JSON
  • coerce types of the JSON properties

Fastify will use the Compiler only in the startup phase to create a serialization function starting from the Schemas provided to the route's response definition, and it will be linked to the route's context.

How to use it

Under the hood, the default Serializer Compiler is fast-json-stringify!
It doesn't expose any options in the Fastify's server options.
In v3 it is customizable via setSerializerCompiler!

Here an example:

fastify.setSerializerCompiler(function (schemaDefinition) {
  const { schema, method, url, httpStatus } = schemaDefinition

  // schema: the JSON schema that has been settle on the route
  // method: the HTTP method of the route
  // url: the complete route's url
  // httpStatus: it is the status settle in the route's `schema.response` option, usually it will be '2xx'

  // return a sync function
  return function (data) {
    // data is the JSON payload

    // now we must return the string that will be sent to the client's request
    return JSON.stringify(data)
  }
})

Note: in Fastify, there is the replySerializer.
It has the priority over the SerializerCompiler in the request's lifecycle, and it will not benefit from the JSON schema boost!

Migrating guidelines

To update your validation and serialization code from v2 to v3 you need to:

  1. update all the schemas that use shared schema replace-way to the standard $ref-way.
  2. replace
    • setSchemaCompiler() to setValidatorCompiler() in the fastify's instances
    • schemaCompiler to validatorCompiler in the routes' definitions
  3. delete the setSchemaResolver()

Update your schemas

Hard things first: to update your schema to remove the shared schema replace-way you have three options:

  1. use the new fastify.getSchema(id)
  2. change the schemas to use $ref keyword
  3. mix the first and second based on your code

Using the fastify.getSchema(id)
is the easiest solution if you have the fastify server instance at your disposal and not too many cyclic replace-way keywords.

fastify.addSchema({
  $id: 'greetings',
  type: 'object',
  properties: {
    hello: { type: 'string' }
  }
})

fastify.route({
  method: 'POST',
  url: '/',
  schema: {
-    body: 'greetings#'
+    body: fastify.getSchema('greetings')
  },
  handler: () => {}
})

Updating the schemas to $ref should be the preferred solution since it is 100% standard and the fastest.
In the official docs there are many
examples of how to use $ref.

It will be like this:

fastify.addSchema({
  $id: 'greetings',
  type: 'object',
  properties: {
    hello: { type: 'string' }
  }
})

fastify.route({
  method: 'POST',
  url: '/',
  schema: {
-    body: 'greetings#'
+    body: { $ref: 'greetings#' }
  },
  handler: () => {}
})

Or you may mix these two options based on your needs.

From schemaCompiler to validatorCompiler

The API for this function has changed, so you need to rename:

  • from schemaCompiler to validatorCompiler in the route's configuration
  • from setSchemaCompiler to setValidatorCompiler in the fastify's instance inizialization
  • all the function's parameters must be changed like this:
-fastify.setSchemaCompiler(function (schema) {
-  return ajv.compile(schema)
+fastify.setValidatorCompiler(function (schemaDefinition) {
+  const { schema, method, url, httpPart } = schemaDefinition
+  return ajv.compile(schema)
})

Or in a stricter sentence:

fastify.post('/the/url', {
  schema: {
    body: joiBodySchema
  },
-  schemaCompiler: schema => data => Joi.validate(data, schema)
+  validatorCompiler: ({ schema }) => data => Joi.validate(data, schema)
}, handler)

Delete schemaResolver

Dropping the shared schema replace-way let fastify to avoid to read and process the JSON schemas, so
it doesn't need to resolve external schemas $id.

Thank you for reading!

Discussion

markdown guide