json-schema-to-typescript [![Build Status][build]](https://github.com/bcherny/json-schema-to-typescript/actions?query=branch%3Amaster+workflow%3ACI) [![npm]](https://www.npmjs.com/package/json-schema-to-typescript) [![mit]](https://opensource.org/licenses/MIT) ![node]
August 28, 2026 · View on GitHub
Compile JSON Schema to TypeScript typings.
Example
Check out the live demo.
Input:
{
"title": "Example Schema",
"type": "object",
"properties": {
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
},
"age": {
"description": "Age in years",
"type": "integer",
"minimum": 0
},
"hairColor": {
"enum": ["black", "brown", "blue"],
"type": "string"
}
},
"additionalProperties": false,
"required": ["firstName", "lastName"]
}
Output:
export interface ExampleSchema {
firstName: string;
lastName: string;
/**
* Age in years
*/
age?: number;
hairColor?: "black" | "brown" | "blue";
}
Installation
npm install json-schema-to-typescript
Usage
json-schema-to-typescript is easy to use via the CLI, or programmatically.
CLI
First make the CLI available using one of the following options:
# install locally, then use `npx json2ts`
npm install json-schema-to-typescript
# or install globally, then use `json2ts`
npm install json-schema-to-typescript --global
# or install to npm cache, then use `npx --package=json-schema-to-typescript json2ts`
# (you don't need to run an install command first)
Then, use the CLI to convert JSON files to TypeScript typings:
cat foo.json | json2ts > foo.d.ts
# or
json2ts foo.json > foo.d.ts
# or
json2ts foo.yaml foo.d.ts
# or
json2ts --input foo.json --output foo.d.ts
# or
json2ts -i foo.json -o foo.d.ts
# or (quote globs so that your shell doesn't expand them)
json2ts -i 'schemas/**/*.json'
# or
json2ts -i schemas/ -o types/
You can pass any of the options described below (including style options) as CLI flags. Boolean values can be set to false using the no- prefix.
The CLI automatically loads the closest Prettier configuration for the generated output file (when writing to stdout: for a .d.ts next to the input file, or in the working directory for piped input). Explicit --style.* flags take precedence over discovered settings, and the output is always parsed as TypeScript whatever parser the config names. A Prettier config that cannot be loaded (invalid syntax, a missing plugin) now fails the run. This does not affect the programmatic API.
# generate code for definitions that aren't referenced
json2ts -i foo.json -o foo.d.ts --unreachableDefinitions
# use single quotes and disable trailing semicolons
json2ts -i foo.json -o foo.d.ts --style.singleQuote --no-style.semi
# pass options to the $ref resolver (quote the flag, so that your shell leaves the `$` alone)
json2ts -i foo.json -o foo.d.ts '--$refOptions.dereference.externalReferenceResolution=root'
Compiling a directory of schemas that reference each other (experimental)
By default each input file is compiled on its own, so a type that several files reach through a $ref is declared again in every output file. With --imports, a directory or glob is compiled together: a type that lives in another file of the set becomes an import type from that file's module instead of a copy.
json2ts -i schemas/ -o types/ --imports
For example, with common.json holding shared definitions, and a.json and b.json using them and each other:
// schemas/common.json
{
"title": "Common",
"type": "object",
"additionalProperties": false,
"definitions": {
"thing": {
"title": "Thing",
"type": "object",
"properties": {"id": {"type": "string"}, "tags": {"type": "array", "items": {"$ref": "#/definitions/tag"}}},
"required": ["id"],
"additionalProperties": false
},
"tag": {"title": "Tag", "type": "string", "enum": ["red", "green"]}
}
}
// schemas/a.json
{
"title": "A",
"type": "object",
"properties": {"thing": {"$ref": "common.json#/definitions/thing"}, "partner": {"$ref": "b.json"}},
"additionalProperties": false
}
// schemas/b.json
{
"title": "B",
"type": "object",
"properties": {"thing": {"$ref": "common.json#/definitions/thing"}, "owner": {"$ref": "a.json"}},
"additionalProperties": false
}
// types/a.d.ts
import type {B} from "./b.js";
import type {Thing} from "./common.js";
export interface A {
thing?: Thing;
partner?: B;
}
// types/b.d.ts
import type {A} from "./a.js";
import type {Thing} from "./common.js";
export interface B {
thing?: Thing;
owner?: A;
}
// types/common.d.ts
/**
* This interface was referenced by `Common`'s JSON-Schema
* via the `definition` "tag".
*/
export type Tag = "red" | "green";
export interface Common {}
/**
* This interface was referenced by `Common`'s JSON-Schema
* via the `definition` "thing".
*/
export interface Thing {
id: string;
tags?: Tag[];
}
(Without --imports, a.d.ts and b.d.ts each declare their own Thing, Tag, A and B, plus a second A1/B1 where the cycle comes back around, and common.d.ts declares only Common.)
What each file gets:
- What a file can import from another is that file's root type, everything under its
definitions/$defs, and any other named schema its root type reaches. A$refto anything else in it (sayother.json#/properties/xwith notitle), to a file outside the set, or to a URL, is declared inline as before; so is a$refthat carries keywords of its own (eg. adescription), since it describes a different type. Schemas under other keys, such as OpenAPI'scomponents/schemas, are not importable yet. - Every file's
definitionsare declared whether or not the file uses them (unreachableDefinitionsis on for the set), so a file's output is whatjson2ts thatFile.json --unreachableDefinitionsprints, minus the declarations that now come from an import, plus theimport typelines. - Each file keeps the type names it would have on its own; where two files disagree, the import renames (
import type {Thing as Thing1}). Files that$refeach other in a cycle are fine. - Import paths are relative, from each output file to the other, and end in
.js, which TypeScript resolves to the.d.ts(or.ts) next to it under everymoduleResolutionsetting. - Relative
$refs resolve against the file they appear in;--cwdcannot be combined with--imports, and--importsneeds an output directory.
The same is available programmatically as compileFiles, which returns the TypeScript for each file, in order, and writes nothing (the output paths are only used to compute the import paths):
import { compileFiles } from 'json-schema-to-typescript'
const [a, b, common] = await compileFiles(
[
{filename: 'schemas/a.json', outputPath: 'types/a.d.ts'},
{filename: 'schemas/b.json', outputPath: 'types/b.d.ts'},
{filename: 'schemas/common.json', outputPath: 'types/common.d.ts'},
],
{bannerComment: ''},
)
API
To invoke json-schema-to-typescript from your TypeScript or JavaScript program, import it and call compile or compileFromFile.
import { compile, compileFromFile } from 'json-schema-to-typescript'
// compile from file
compileFromFile('foo.json')
.then(ts => fs.writeFileSync('foo.d.ts', ts))
// or, compile a JS object
let mySchema = {
properties: [...]
}
compile(mySchema, 'MySchema')
.then(ts => ...)
See server demo and browser demo for full examples.
Options
compileFromFile and compile accept options as their last argument (all keys are optional):
| key | type | default | description |
|---|---|---|---|
| additionalProperties | boolean | true | Default value for additionalProperties, when it is not explicitly set |
| bannerComment | string | "/* eslint-disable */\n/**\n* This file was automatically generated by json-schema-to-typescript.\n* DO NOT MODIFY IT BY HAND. Instead, modify the source JSON Schema file,\n* and run json-schema-to-typescript to regenerate this file.\n*/" | Disclaimer comment prepended to the top of each generated file |
| customName | (LinkedJSONSchema, string | undefined) => string | undefined | undefined | Custom function to provide a type name for a given schema |
| cwd | string | process.cwd() | Root directory for resolving $refs (for compileFiles: the directory its relative filenames and outputPaths are taken from; $refs then resolve against each file) |
| declarationStyle | 'interface' | 'type' | 'interface' | Declare object types as interfaces (export interface B extends A {…}) or as type aliases (export type B = A & {…}) |
| declareExternallyReferenced | boolean | true | Declare external schemas referenced via $ref? |
| enableConstEnums | boolean | true | Prepend enums with const? |
| inferStringEnumKeysFromValues | boolean | false | Create enums from JSON enums with eponymous keys |
| format | boolean | true | Format code? Set this to false to improve performance. |
| formatTypes | Record<string, string> | {} | Map from a string schema's format to the TypeScript type to emit for it, verbatim, like tsType (which still takes precedence, as do enum and const). Eg. { 'date-time': 'Date' } turns { "type": "string", "format": "date-time" } into Date instead of string; nullable, arrays and $refs follow (Date | null, Date[]). Formats you don't list stay string. For a type of your own, add its import via bannerComment. CLI: --formatTypes.date-time=Date. |
| ignoreMinAndMaxItems | boolean | false | Ignore maxItems and minItems for array types, preventing tuples being generated. |
| maxItems | number | 20 | Maximum number of unioned tuples to emit when representing bounded-size array types, before falling back to emitting unbounded arrays. Increase this to improve precision of emitted types, decrease it to improve performance, or set it to -1 to ignore maxItems. |
| readonly | boolean | false | Mark every property and index signature readonly, and emit every array and tuple type as readonly T[]. |
| readonlyKeyword | boolean | false | Map the schema's readOnly: true annotation to TypeScript's readonly: an annotated property gets the readonly modifier, and an annotated array or tuple is emitted as readonly T[]. |
| removeOptionalIfDefaultExists | boolean | false | Remove the optional modifier when a property has a default value. |
| strictIndexSignatures | boolean | false | Append all index signatures with | undefined so that they are strictly typed. |
| style | object | { bracketSpacing: false, printWidth: 120, semi: true, singleQuote: false, tabWidth: 2, trailingComma: 'none', useTabs: false } | A Prettier configuration |
| undefinedOptionalProperties | boolean | false | Append | undefined to the type of every optional property (age?: number | undefined), for consumers that compile with TypeScript's exactOptionalPropertyTypes. |
| unknownAny | boolean | true | Use unknown instead of any where possible |
| unreachableDefinitions | boolean | false | Generates code for $defs that aren't referenced by the schema. |
| $refOptions | object | {} | [ref`s |
Tests
This repo uses bun (1.3.9 or later) to install dependencies and run its scripts and tests, so install it first:
$ bun install
$ bun run test
Features
-
title=>interface - Primitive types:
- array
- homogeneous array
- boolean
- integer
- number
- null
- object
- string
- homogeneous enum
- heterogeneous enum
- Non/extensible interfaces
- Custom JSON-schema extensions
- Nested properties
- Schema definitions
- Schema references
- Local (filesystem) schema references
- External (network) schema references
- Add support for running in browser
- default interface name
- infer unnamed interface name from filename
-
deprecated -
allOf("intersection") -
anyOf("union") -
oneOf(treated likeanyOf) -
maxItems(eg) -
minItems(eg) - tuples: array-form
items+additionalItems(draft 4 – 2019-09) andprefixItems+items(draft 2020-12) -
additionalPropertiesof type -
patternProperties(partial support) -
extends -
requiredproperties on objects (eg) -
validateRequired(draft 3 stylerequired: trueon a property) (eg) - literal objects in enum (eg)
- referencing schema by id (eg)
- custom typescript types via
tsType -
readOnly→readonlyproperties and arrays (readonlyKeywordoption)
Custom schema properties:
tsType: Overrides the type that's generated from the schema. Useful for forcing a type toanyor when using non-standard JSON schema extensions (eg).tsEnumNames: Overrides the names used for the elements in an enum. Can also be used to create string enums (eg).
Not expressible in TypeScript:
dependencies(single, multiple)divisibleBy(eg)format(eg) — but see theformatTypesoption to map a format to a type of your choosingmultipleOf(eg)maximum(eg)minimum(eg)maxProperties(eg)minProperties(eg)not/disallowoneOf("xor", useanyOfinstead)pattern(string, regex)uniqueItems(eg)
JSON Schema draft support
Every draft goes through the same pipeline; a $schema declaration does not change the output. The schema model is draft 4 (JSONSchema4 from @types/json-schema): the lists above cover its keywords, the table below covers what later drafts added. "Supported" means the keyword shapes the emitted type as the spec intends; keywords that only constrain values have no TypeScript equivalent and are ignored next to a type (a subschema made of nothing else still comes out as {[k: string]: unknown} rather than unknown, #806 pending). As of master d86f285, which is ahead of 16.0.0 (there unevaluatedProperties is still ignored and formatTypes does not exist); every row was checked by compiling a one-keyword schema with the CLI.
| Keyword | Status | Note | Tracking |
|---|---|---|---|
| draft 6 | |||
const | supported | literal types, including object literals | |
boolean schemas (true / false) | supported | as a property, items, additionalProperties, and inside allOf / anyOf. A $ref to one crashes (#809 pending); a root true / false errors | #725, #496 |
$id | supported | for naming, and $ref: "#name" to an $id: "#name" | |
examples | ignored | not copied into the JSDoc comment | #237 |
propertyNames | ignored | enum / const names would be expressible | #337 |
contains, numeric exclusiveMinimum / exclusiveMaximum | not expressible | ||
| draft 7 | |||
if / then / else | ignored | properties and required inside the branches contribute nothing | #426 |
readOnly | ignored | a readonly modifier is #796 (pending) | #131 |
writeOnly, $comment, contentMediaType, contentEncoding, new formats | not expressible | formats are plain string unless mapped with the formatTypes option | |
| 2019-09 | |||
$defs | supported | same as definitions (using both in one schema errors) | |
$anchor | errors | a $ref: "#name" to it fails with "Refs should have been resolved by the resolver"; $id: "#name" or a JSON pointer works | |
$id inside a subschema as a base URI | errors | $refs resolve against the file's location, not against an enclosing $id | |
$ref with sibling keywords | partial | merged into a copy of the referenced schema, not intersected with it: a keyword both sides have (eg. properties) keeps one side only, and the definition may be emitted twice (Foo, Foo1). Use allOf to get both. Annotation-only siblings stop forking with #803 (pending) | |
$recursiveRef / $recursiveAnchor | ignored | the target is typed {[k: string]: unknown} (unknown with #806, pending), not the recursive type | |
unevaluatedProperties | partial | like additionalProperties for the schema's own properties; not enforced across allOf / anyOf / oneOf; wrongly closed over properties from a sibling $ref, dependentSchemas or if / then (#798 pending) | |
unevaluatedItems, dependentSchemas | ignored | ||
dependentRequired, minContains / maxContains, contentSchema | not expressible | like draft 4 dependencies | #169 |
deprecated | supported | @deprecated in the JSDoc comment | |
| 2020-12 | |||
prefixItems | ignored | unknown[]; with items: false → never[], with items: {…} that schema is applied to every element. Tuples are #816 (pending) | #543 |
$dynamicRef / $dynamicAnchor | ignored | as $recursiveRef |
Not supported from 2019-09 / 2020-12, by name: $anchor, $recursiveRef, $recursiveAnchor, $dynamicRef, $dynamicAnchor, $vocabulary, unevaluatedItems, prefixItems, dependentSchemas, dependentRequired, minContains, maxContains, contentSchema, and $id-based reference resolution.
FAQ
JSON-Schema-to-TypeScript is crashing on my giant file. What can I do?
Prettier is known to run slowly on really big files. To skip formatting and improve performance, set the format option to false.
Further Reading
- JSON Schema specification (2020-12, with links to every earlier draft): https://json-schema.org/specification
- Draft 4, the schema model used here: https://json-schema.org/draft-04/draft-zyp-json-schema-04
- What changed per draft: draft 6, draft 7, 2019-09, 2020-12
- Understanding JSON Schema: https://json-schema.org/understanding-json-schema
- JSON Schema test suite: https://github.com/json-schema-org/JSON-Schema-Test-Suite
- TypeScript handbook: https://www.typescriptlang.org/docs/handbook/