Intro
This post briefly tells the story of why I wrote 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
to infer TypeScript types from the generated JSON schema files. This is a quite common pattern documented by Ajv and Fastify, to name a few:
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 is a "machine-readable interface definition language for describing, [...] web services", and TypeScript 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
and OpenAPI -> JSON schema is covered by 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)
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
.
The generation flow consists of:
- Resolve external/remote
$ref
s and dereference them with@apidevtools/json-schema-ref-parser
(resolving$ref
's) - Convert to JSON schema with
@openapi-contrib/openapi-schema-to-json-schema
andopenapi-jsonschema-parameters
- Generate one TypeScript JSON schema file for each definition (
.ts
files withas const
assertion) - 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:
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
)
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
and start a discussion there in case something doesn't fit your use case.
Top comments (0)