DEV Community

Cover image for Robust Data Integrity: Node.js JSON Schema Validation with TypeBox and TypeScript
Francisco Mendes
Francisco Mendes

Posted on • Edited on

Robust Data Integrity: Node.js JSON Schema Validation with TypeBox and TypeScript

Introduction

In today's article I will explain how we can define a JSON Schema and perform its data validation using the TypeBox library in a Node environment with TypeScript.

Anyone who has gone through the frustration of creating a set of interfaces and enums and then translating it to a JSON Schema knows how hard it is to do this conversion, although there are libraries that already generate data types for JSON Schema.

And sometimes we either have to create our own generics or we are literally limited with what is provided to us by the libraries, with which we often end up spending more time solving problems related to data types than actually solving the real problem.

For these same reasons I like to use TypeBox, because I feel that the TypeScript support is first-class citizen.

Getting Started

In a Node environment with TypeScript that you already have, install the following dependency:

npm install @sinclair/typebox --save
Enter fullscreen mode Exit fullscreen mode

For the example of today's article, let's create a schema with only three properties that will be required, as follows:

import { Type, Static } from "@sinclair/typebox";

export const profileSchema = Type.Object({
  firstName: Type.String(),
  lastName: Type.String(),
  age: Type.Integer(),
});
Enter fullscreen mode Exit fullscreen mode

The schema created above is equivalent to the following JSON Schema:

{
   "type":"object",
   "properties":{
      "firstName":{
         "type":"string"
      },
      "lastName":{
         "type":"string"
      },
      "age":{
         "type":"integer"
      }
   },
   "required":[
      "firstName",
      "lastName",
      "age"
   ]
}
Enter fullscreen mode Exit fullscreen mode

Now, from the schema that was created, let's create a static data type:

import { Type, Static } from "@sinclair/typebox";

export const profileSchema = Type.Object({
  firstName: Type.String(),
  lastName: Type.String(),
  age: Type.Integer(),
});

// 👇 added this line
export type ProfileSchemaType = Static<typeof profileSchema>; 
Enter fullscreen mode Exit fullscreen mode

Then we can create a small factory, which will receive a schema as the only argument and as a return it will have a "copy" of the schema that was passed in the arguments and a validation function.

In this validation function we will receive as the only argument the data whose properties we want to validate, if they are valid we return the same data, otherwise we throw an error. This way:

import { TObject } from "@sinclair/typebox";
import { TypeCompiler } from "@sinclair/typebox/compiler";

interface ValidatorFactoryReturn<T> {
  schema: TObject;
  verify: (data: T) => T;
}

export const validatorFactory = <T extends unknown>(
  schema: TObject
): ValidatorFactoryReturn<T> => {
  const C = TypeCompiler.Compile(schema);

  const verify = (data: T): T => {
    const isValid = C.Check(data);
    if (isValid) {
      return data;
    }
    throw new Error(
      JSON.stringify(
        [...C.Errors(data)].map(({ path, message }) => ({ path, message }))
      )
    );
  };

  return { schema, verify };
};
Enter fullscreen mode Exit fullscreen mode

Finally, we can instantiate our factory passing the schema we created in the arguments and then we can validate the data we want using the .verify() function.

To have a clearer example, if you want to validate the data from the body of the http request, you can use it as follows:

import Koa from "koa";
import Router from "@koa/router";
import koaBody from "koa-body";

import { profileSchema, ProfileSchemaType } from "./schema";
import { validatorFactory } from "./validator";

const profileValidation = validatorFactory<ProfileSchemaType>(profileSchema);

const app = new Koa();
const router = new Router();

app.use(koaBody());

router.post("/", (ctx) => {
  const body = ctx.request.body as ProfileSchemaType;
  const data = profileValidation.verify(body);
  ctx.body = { data };
});

app.use(router.routes());

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

And in the http request body we can send the following object:

{
  "firstName": "Francisco",
  "lastName": "Mendes",
  "job": "Full Stack Dev"
}
Enter fullscreen mode Exit fullscreen mode

As you can expect, you will receive an error, most likely a 500 because the job property is not defined in the schema and the age property is missing. But if a correct object is sent, it is expected that the response will be the same as the object that was sent.

Conclusion

As usual, I hope you enjoyed the article and that it helped you with an existing project or simply wanted to try it out.

If you found a mistake in the article, please let me know in the comments so I can correct it. Before finishing, if you want to access the source code of this article, I leave here the link to the github repository.

Top comments (0)