Generating JSON Schema from TypeScript types

mangelosanto

Matt Angelosanto

Posted on July 11, 2023

Generating JSON Schema from TypeScript types

Written by Ikeh Akinyemi✏️

In modern web development, it’s common to work with complex data that must be transmitted between different applications. JSON has proven to be an excellent standardized serialization process for exchanging wire data between applications.

However, with this increased complexity in wire data comes a corresponding lack of security. While JSON is flexible, it doesn’t provide any protection for your data. We then face the challenge of ensuring that the data we transmit between applications adhere to a known schema.

This uncertainty can prove problematic, especially for applications that take data security very seriously. So how do we address this?

JSON Schema provides a solution to the aforementioned security concern. It allows us to define a strict structure for the wire data transmitted between communicating applications.

Throughout this article, we’ll explore how to generate and work with JSON Schema using TypeScript types. Jump ahead:

You can check out the full project code in this GitHub repository.

Why use JSON Schema and TypeScript?

Before we delve into JSON Schema as a solution, let’s first answer the question, what exactly is JSON Schema?

JSON Schema is an established specification that helps us to define the structure of JSON data. We define what properties the data should have, what data type we expect those properties should be, and how they should be validated.

By using the Schema, we ensure that the wire data our application is accepting is formatted correctly and that any possible errors are caught early in our development process. So, where does TypeScript fit into the picture?

TypeScript provides a static type-checking feature that validates the type consistency of variables, function parameters, and return values at compile-time. By using tools that can convert TypeScript types into JSON Schemas, we can ensure our wire data adheres to a strict schema.

Generating JSON Schema from TypeScript Types

In this section, we will explore how we can generate JSON Schema from our defined TypeScript types we’ll define.

We will use the ts-json-schema-generator library, which supports a wide range of TypeScript features, including interfaces, classes, enums, unions, and more. However, it’s worth noting that there are various other developer tools you can use to achieve the same result.

Project setup

Let’s start by creating a new project folder named json-schema-project and running an init command within the new directory to initialize a new project:

mkdir json-schema-project; cd json-schema-project;
npm init -y
Enter fullscreen mode Exit fullscreen mode

Now we have set up our project, let’s install the ts-json-schema-generator library:

npm install -g ts-json-schema-generator
Enter fullscreen mode Exit fullscreen mode

Once we've installed the library, we can use it to generate JSON Schema from our TypeScript types.

Writing a TypeScript interface

Let's start by creating a simple TypeScript file named types.ts within the src folder. Within this file, we'll write a simple TypeScript interface:

export interface Person {
  firstName: string;
  lastName: string;
  age: number;
  socials: string[];
}
Enter fullscreen mode Exit fullscreen mode

In the above snippet, we defined a Person object using TypeScript’s interface. Note the types of each property within the interface.

Using the ts-json-schema-generator package

Next, let’s use the ts-json-schema-generator package to generate a JSON Schema for the Person interface. We will use this command:

ts-json-schema-generator --path 'src/types.ts' --type 'Person'
Enter fullscreen mode Exit fullscreen mode

This command will instruct the ts-json-schema-generator package to generate a JSON Schema for the Person type in the src/types.ts file.

If the command runs successfully, the result will be the below JSON object that represents the generated JSON Schema:

{
  "$ref": "#/definitions/Person",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "definitions": {
    "Person": {
      "additionalProperties": false,
      "properties": {
        "age": {
          "type": "number"
        },
        "firstName": {
          "type": "string"
        },
        "lastName": {
          "type": "string"
        },
        "socials": {
          "items": {
            "type": "string"
          },
          "type": "array"
        }
      },
      "required": [ "firstName", "lastName", "age", "socials"],
      "type": "object"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, with this schema, we can specify that any object that claims to be of the type Person should have certain properties. These include firstName and lastName properties of type string, an age property of type number, and a socials property comprising an array of strings.

To further understand the JSON Schema, let’s take a look at different fields of the JSON Schema object and what they stand for:

  • $schema: Specifies the version of the JSON Schema specification being used — in this case, v7
  • type: Specifies the type of object being described by the schema — in this case, an object
  • properties: An object that describes the properties of the object being described by the schema — in this case, name, age, and hobbies
  • required: An array that specifies which properties of the object are required — in this case, all three properties are required

Using the generated JSON Schema

Now that we have seen how to generate JSON Schema from TypeScript, we can go further to see how to use it and validate data ensuring it conforms to the expected schema that we have.

For the following example, we will programmatically use the ts-json-schema-generator package alongside the ajv schema validation package:

import Ajv from 'ajv';
import { createGenerator } from 'ts-json-schema-generator';
import path from 'path';

const repoRoot = process.cwd();
const config = {
  path: path.join(repoRoot, "src", "types.ts"),
  tsconfig: path.join(repoRoot, "tsconfig.json"),
  type: "Person",
};

const schema = createGenerator(config).createSchema("Person")
const validate = (data: any) => {
  const ajv = new Ajv();
  const validateFn = ajv.compile(schema);
  return validateFn(data);
};

const person = {
  firstName: 'Alice',
  lastName: 'Chapman',
  age: 30,
  socials: ['github', 'twitter']
};

console.log(validate(person)); // true
Enter fullscreen mode Exit fullscreen mode

In the above snippet, we defined a few configurations:

  • One to see the file path containing our TypeScript type definitions
  • Another for the file path to the tsconfig.json file
  • The last one to specify what type we want to generate JSON Schema for

Note that if we want to generate JSON Schema for all types and we have more than one type definition, we can use * as the value instead of Person.

Using the createSchema method of the SchemaGenerator type, we create a JSON Schema object for the Person type and store it in the constant variable, schema.

Then, within the validate function, we use the compile method on the Ajv object to compile the JSON Schema. We then use the resulting validator function — validateFn — to validate any data sample.

Adding a middleware function to validate our data

To see a more practical use case, let’s modify the previous snippet by adding a middleware function to a web server. This middleware will validate the wire data against the existing JSON Schema we have:

// --snip--
import express, { Request, Response, NextFunction } from 'express';
import { Person } from "./types";

const repoRoot = process.cwd();
const configType = "Person";
const config = {
  path: path.join(repoRoot, "src", "types.ts"),
  tsconfig: path.join(repoRoot, "tsconfig.json"),
  type: configType,
};

const schema = createGenerator(config).createSchema(configType);
const app = express();

app.use(express.json());

// Middleware to validate incoming payload against JSON Schema
const validatePayload = (req: Request, res: Response, next: NextFunction) => {
  const ajv = new Ajv();
  const validateFn = ajv.compile(schema);
  if (!validateFn(req.body)) {
    return res.status(400).json({ error: 'Invalid payload' });
  }
  next();
};

// Endpoint that accepts User payloads
app.post('/users', validatePayload, (req: Request, res: Response) => {
  const newUser = req.body as Person;
  // Do something with newUser
  res.json(newUser);
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
Enter fullscreen mode Exit fullscreen mode

The above snippet defined a middleware — validatePayload — that checks if the incoming payload conforms to the generated JSON Schema. If not, it returns a 400 error response.

Then, within the POST /users endpoint, we use the middleware to verify that the wire data sent conforms to the schema definition. This way, we’re able to prevent errors that can happen as a result of unexpected payloads, thereby improving the overall reliability of the API.

Start the web server using the below command:

npx ts-node .
Enter fullscreen mode Exit fullscreen mode

Make sure to set the value of the main field in the package.json file to src/index.ts. We’ll use Postman to test our implementation: Postman Interface Showing Test Calling Endpoint With Unexpected Request Resulting In 400 Error Response In the above screenshot, we can see the result of calling our endpoint with an unexpected request body results in a 400 error response, as it should. Next, let’s send the expected payload to the endpoint: Postman Interface Showing Test Calling Endpoint With Expected Payload Resulting In 200 Status Response As we can see, we get the expected 200 status response, indicating a successful request.

Conclusion

We looked at how to create JSON Schema from TypeScript types in this article.

We can guarantee that our data follows a rigorous schema and take advantage of TypeScript's static type-checking by utilizing TypeScript types to build JSON Schema. This ensures that our data is both well-structured and type-safe.

Generating JSON Schema from TypeScript types can help prevent errors both client-side and server-side, as any data that does not conform to the schema will be rejected. It also helps with documentation, as clients can easily see the expected structure of the data they need to send.

Here’s the GitHub repository containing the full source files for the code examples in this article.


LogRocket: Full visibility into your web and mobile apps

LogRocket Signup

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

Try it for free.

💖 💪 🙅 🚩
mangelosanto
Matt Angelosanto

Posted on July 11, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related