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:
- Why use JSON Schema and TypeScript?
- Generating JSON Schema from TypeScript types
- Using the generated JSON Schema
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
Now we have set up our project, let’s install the ts-json-schema-generator
library:
npm install -g ts-json-schema-generator
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[];
}
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'
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"
}
}
}
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, anobject
-
properties
: An object that describes the properties of the object being described by the schema — in this case,name
,age
, andhobbies
-
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
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');
});
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 .
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: 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: 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 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.
Top comments (2)
...why not just use zod
I wonder if zod can be converted to JSON Schema. JSON schema is very useful for documentation, especially in OpenAPI setups. In Fastify they use Typebox instead of Zod because of this.