Align TypeScript Standards With JSON Schema
Tags: json, typescript, api, node
Published: 2020-09-20 21:48:24


In my current work, I am looking at ways to try to enforce particular standards across multiple and larger projects.

This means standards set across different languages that are flexible, extensible and kept up to date.

A few of my upcoming posts will be looking into some of my spikes that I am doing to as investigation, starting with the `json-schema-to-typescript` library.

## JSON Schema

What is JSON Schema? Here is a definition from the [JSON Schema Org site](https://json-schema.org/understanding-json-schema/index.html):

> JSON Schema is a powerful tool for validating the structure of JSON data.

The hope is that I can use tooling for JSON schema and [Open API](https://www.openapis.org/) to help with structuring micro-services and providing "cheap" contract testing.

## Setting up the project

```s
mkdir json-schema-ts-spike
cd json-schema-ts-spike

# start a yarn project with default settings
yarn init -y
yarn add json-schema-to-typescript jsonschema

# setup files we will use
touch index.js book.json
```

## Compiling From Source

In my example, I will opt to generate by reading in from a particular file.

```js
const Validator = require("jsonschema").Validator
const { compile, compileFromFile } = require("json-schema-to-typescript")
const fs = require("fs")
const path = require("path")

const main = async () => {
  // validate the schema first
  const v = new Validator()

  // read the schema details
  const schemaFilepath = path.join(__dirname, "book.json")
  const bookSchema = JSON.parse(fs.readFileSync(schemaFilepath, "utf-8"))

  // read the example
  const exampleJsonFilepath = path.join(__dirname, "example.json")
  const exampleJson = JSON.parse(fs.readFileSync(exampleJsonFilepath, "utf-8"))

  v.addSchema(bookSchema, "/BookSchema")
  const validation = v.validate(exampleJson, bookSchema)

  if (validation.errors.length) {
    console.log(validation.errors)
    process.exit(1)
  }

  // compile from file
  const ts = await compileFromFile(schemaFilepath)
  fs.writeFileSync("book.d.ts", ts)
}

main()
```

This will be all the code we need for our example.

## The JSON Schema file

For this part, let's model a basic book and a collection. We need to add some schema info the the `book.json` file.

I won't go too deep into the modelling itself with JSON schema, but these are the definitions I am coming up with:

```json
{
  "$schema": "http://json-schema.org/draft-07/schema#",

  "definitions": {
    "user": {
      "type": "object",
      "properties": {
        "name": { "type": "string" },
        "preferredName": { "type": "string" },
        "age": { "type": "string" },
        "gender": { "enum": ["male", "female", "other"] }
      },
      "required": ["name", "preferredName", "age", "gender"]
    },
    "author": {
      "type": "object",
      "properties": {
        "allOf": [{ "$ref": "#/definitions/address" }]
      }
    }
  },
  "type": "object",
  "properties": {
    "author": { "$ref": "#/definitions/author" },
    "title": { "type": "string" },
    "publisher": { "type": "string" }
  },
  "required": ["author", "title", "publisher"]
}
```

## The Book JSON

Let's add some info to our basic `example.json` file that we can test against:

```json
{
  "author": {
    "name": "Dennis O'Keeffe",
    "preferredName": "Dennis",
    "age": 28,
    "gender": "male"
  },
  "title": "The Greatness Of Strict Schemas",
  "publisher": "Real Publisher (definitely not fake)"
}
```

## Running Our Creation

Run `node index.js` from the root directory.

You will actually notice that I left a mistake in there! The following will log out:

```s
> node index.js
[
  ValidationError {
    property: 'instance.author.age',
    message: 'is not of a type(s) string',
    schema: { type: 'string' },
    instance: 28,
    name: 'type',
    argument: [ 'string' ],
    stack: 'instance.author.age is not of a type(s) string'
  }
]
```

Our validation (or invalidation per se) was a success! We said in the schema that it should be a string but we received the number `28`.

Head back to `book.json` and convert the value to type `number`. Now if we run it again `node index.js` again, we will get some success! We will even see our `books.d.ts` file has been written.

> You might be wondering why I am writing this blog post in `js` to generate TypeScript types. Honestly, when I am doing spikes I normally just write quick scripts and keep out the fluff.

You will see the following is generated:

```ts
/* tslint:disable */
/**
 * This file was automatically generated by json-schema-to-typescript.
 * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
 * and run json-schema-to-typescript to regenerate this file.
 */

export interface BookSchema {
  author: User
  title: string
  publisher: string
  [k: string]: unknown
}
export interface User {
  name: string
  preferredName: string
  age: number
  gender: "male" | "female" | "other"
  [k: string]: unknown
}
```

Great success! We now have a type for our schema that we can import in.

## Resources and Further Reading

1. [jsonschema - GitHub](https://github.com/tdegrunt/jsonschema)
2. [JSON Schema - Structuring A Complex Schema](https://json-schema.org/understanding-json-schema/structuring.html)

_Image credit: [Tony Pham](https://unsplash.com/@tonyphamvn)_

_Originally posted on my [blog](https://blog.dennisokeeffe.com/blog/2020-09-20-json-schema-to-ts/). Follow me on Twitter for more hidden gems [@dennisokeeffe92](https://twitter.com/dennisokeeffe92)._