Unlocking API Interoperability: Converting OpenAPI to TypeScript JSON Schema
Tags: openapi, typescript, node, javascript
Published: 2023-09-04 07:51:44

## Intro

This post briefly tells the story of why I wrote [`openapi-ts-json-schema`](https://github.com/toomuchdesign/openapi-ts-json-schema) to fill the unexpected gap existing between **OpenAPI specification** and **TypeScript JSON schema** format.

## Prologue

In the last few weeks at work I was faced with an obvious task which I thought had an obvious technical solution, from my naive fronted engineer background:

> Given an **OpenAPI Specification**, generate the relevant **JSON schema** files to be consumed and type-interpreted by a **TypeScript** application.

The main idea consists of leveraging a library like [`json-schema-to-ts`](https://www.npmjs.com/package/json-schema-to-ts) to infer TypeScript types from the generated JSON schema files. This is a quite common pattern documented by [Ajv](https://ajv.js.org/guide/typescript.html#utility-type-for-jtd-data-type) and [Fastify](https://fastify.dev/docs/latest/Reference/Type-Providers
), to name a few:

- [**Ajv** example](https://codesandbox.io/p/devbox/cranky-euler-yhv7l8?file=%2Fsrc%2Fajv.ts%3A3%2C1)
- [**Fastify** example](https://codesandbox.io/p/devbox/cranky-euler-yhv7l8?file=%2Fsrc%2Fajv.ts%3A3%2C1)

```ts
import Ajv from 'ajv';
import type { FromSchema } from 'json-schema-to-ts';
import mySchema from 'path/to/generated/schemas/MyModel.ts';

const ajv = new Ajv();
// Perform data validation and type inference using the same schema
const validate = ajv.compile<FromSchema<typeof mySchema>>(mySchema);
const data: unknown = {};

if (validate(data)) {
  // data gets type inference
  console.log(data.foo);
} else {
  console.log(validate.errors);
}
```

Since the [OpenAPI Specification](https://swagger.io/specification/) is a _"machine-readable interface definition language for describing, [...] web services"_, and [TypeScript](https://www.typescriptlang.org/) is practically the only available type solution for Node.js, I confidently googled:

> "openAPI to typescript JSON schema"

...and found no relevant results.

## OpenAPI to TypeScript JSON schema gap

If **OpenAPI -> TypeScript types** is mostly covered by [`openapi-typescript`](https://www.npmjs.com/package/openapi-typescript
) and **OpenAPI -> JSON schema** is covered by [`openapi-schema-to-json-schema`](https://www.npmjs.com/package/@openapi-contrib/openapi-schema-to-json-schema), **there is no clear way to generate TypeScript JSON schemas from an OpenAPI definition**.

The main shortcomings consist of TypeScript not being able to:
- Interpret/resolve JSON schema's `$ref` and `$id` props
- Import JSON files `as const` (See [relevant GitHub thread](https://github.com/microsoft/TypeScript/issues/32063))

```ts
const dogSchema = {
  type: "object",
  properties: {
    name: { type: "string" },
    // TS cannot interpret/resolve $refs
    favoriteFood: { "$ref": "components/schemas/Food" },
  },
  required: ["name", "favoriteFood"],
} as const;
```

Considering the example above, in order to have a JSON schema which TypeScript can properly interpret, all `$ref` entries should be somehow statically resolved at generation time.

After an initial attempt to manually replace the `$ref`s all around my JSON schemas, I ended up putting together a few lines to automate the process which I eventually abstracted into a JS NPM package: [`openapi-ts-json-schema`](https://github.com/toomuchdesign/openapi-ts-json-schema).

The generation flow consists of:

1. Resolve external/remote `$ref`s and dereference them with [`@apidevtools/json-schema-ref-parser`](https://github.com/APIDevTools/json-schema-ref-parser) (resolving `$ref`'s)
2. Convert to JSON schema with [`@openapi-contrib/openapi-schema-to-json-schema`](https://github.com/openapi-contrib/openapi-schema-to-json-schema) and [`openapi-jsonschema-parameters`](https://www.npmjs.com/package/openapi-jsonschema-parameters)
3. Generate one TypeScript JSON schema file for each definition (`.ts` files with `as const` assertion)
4. Store schemas in a folder structure reflecting the original OpenAPI definition structure

The example above would result into a **TypeScript JSON schema** file, ready to be imported and consumed:

```ts
import componentsSchemasFood from "./../components/schemas/Food"

export default {
  type: "object",
  properties: {
    name: { type: "string" },
    favoriteFood: componentsSchemasFood,
  },
  required: ["name", "favoriteFood"],
} as const;
```

Since v0.3.0, `$ref` definitions get generated as standalone TS JSON schema files and imported where needed.

## Conclusion

Given the centrality of JSON schemas in most of web development, I would be glad to see TypeScript taking over and make JSON schemas first class citizens by adding native support for:

- importing JSON schema import as const
- inferring types natively from any JSON schema (currently achieved with the outstanding [`json-schema-to-ts`](https://www.npmjs.com/package/json-schema-to-ts))

TypeScript JSON schemas are 100% valid JSON schemas and would be able to represent the missing joining link between **runtime type validation** and **static type check**.

In the meanwhile feel free to take a look at the approach taken by [`openapi-ts-json-schema`](https://github.com/toomuchdesign/openapi-ts-json-schema) and start a discussion there in case something doesn't fit your use case.