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 $ref to anything else in it (say other.json#/properties/x with no title), to a file outside the set, or to a URL, is declared inline as before; so is a $ref that carries keywords of its own (eg. a description), since it describes a different type. Schemas under other keys, such as OpenAPI's components/schemas, are not importable yet.
  • Every file's definitions are declared whether or not the file uses them (unreachableDefinitions is on for the set), so a file's output is what json2ts thatFile.json --unreachableDefinitions prints, minus the declarations that now come from an import, plus the import type lines.
  • 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 $ref each 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 every moduleResolution setting.
  • Relative $refs resolve against the file they appear in; --cwd cannot be combined with --imports, and --imports needs 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):

keytypedefaultdescription
additionalPropertiesbooleantrueDefault value for additionalProperties, when it is not explicitly set
bannerCommentstring"/* 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 | undefinedundefinedCustom function to provide a type name for a given schema
cwdstringprocess.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 & {…})
declareExternallyReferencedbooleantrueDeclare external schemas referenced via $ref?
enableConstEnumsbooleantruePrepend enums with const?
inferStringEnumKeysFromValuesbooleanfalseCreate enums from JSON enums with eponymous keys
formatbooleantrueFormat code? Set this to false to improve performance.
formatTypesRecord<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.
ignoreMinAndMaxItemsbooleanfalseIgnore maxItems and minItems for array types, preventing tuples being generated.
maxItemsnumber20Maximum 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.
readonlybooleanfalseMark every property and index signature readonly, and emit every array and tuple type as readonly T[].
readonlyKeywordbooleanfalseMap 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[].
removeOptionalIfDefaultExistsbooleanfalseRemove the optional modifier when a property has a default value.
strictIndexSignaturesbooleanfalseAppend all index signatures with | undefined so that they are strictly typed.
styleobject{ bracketSpacing: false, printWidth: 120, semi: true, singleQuote: false, tabWidth: 2, trailingComma: 'none', useTabs: false }A Prettier configuration
undefinedOptionalPropertiesbooleanfalseAppend | undefined to the type of every optional property (age?: number | undefined), for consumers that compile with TypeScript's exactOptionalPropertyTypes.
unknownAnybooleantrueUse unknown instead of any where possible
unreachableDefinitionsbooleanfalseGenerates code for $defs that aren't referenced by the schema.
$refOptionsobject{}[RefParser](https://github.com/APIDevTools/jsonschemarefparser)Options,usedwhenresolvingRefParser](https://github.com/APIDevTools/json-schema-ref-parser) Options, used when resolving `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 like anyOf)
  • maxItems (eg)
  • minItems (eg)
  • tuples: array-form items + additionalItems (draft 4 – 2019-09) and prefixItems + items (draft 2020-12)
  • additionalProperties of type
  • patternProperties (partial support)
  • extends
  • required properties on objects (eg)
  • validateRequired (draft 3 style required: true on a property) (eg)
  • literal objects in enum (eg)
  • referencing schema by id (eg)
  • custom typescript types via tsType
  • readOnlyreadonly properties and arrays (readonlyKeyword option)

Custom schema properties:

  • tsType: Overrides the type that's generated from the schema. Useful for forcing a type to any or 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 the formatTypes option to map a format to a type of your choosing
  • multipleOf (eg)
  • maximum (eg)
  • minimum (eg)
  • maxProperties (eg)
  • minProperties (eg)
  • not/disallow
  • oneOf ("xor", use anyOf instead)
  • 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.

KeywordStatusNoteTracking
draft 6
constsupportedliteral types, including object literals
boolean schemas (true / false)supportedas a property, items, additionalProperties, and inside allOf / anyOf. A $ref to one crashes (#809 pending); a root true / false errors#725, #496
$idsupportedfor naming, and $ref: "#name" to an $id: "#name"
examplesignorednot copied into the JSDoc comment#237
propertyNamesignoredenum / const names would be expressible#337
contains, numeric exclusiveMinimum / exclusiveMaximumnot expressible
draft 7
if / then / elseignoredproperties and required inside the branches contribute nothing#426
readOnlyignoreda readonly modifier is #796 (pending)#131
writeOnly, $comment, contentMediaType, contentEncoding, new formatsnot expressibleformats are plain string unless mapped with the formatTypes option
2019-09
$defssupportedsame as definitions (using both in one schema errors)
$anchorerrorsa $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 URIerrors$refs resolve against the file's location, not against an enclosing $id
$ref with sibling keywordspartialmerged 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 / $recursiveAnchorignoredthe target is typed {[k: string]: unknown} (unknown with #806, pending), not the recursive type
unevaluatedPropertiespartiallike 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, dependentSchemasignored
dependentRequired, minContains / maxContains, contentSchemanot expressiblelike draft 4 dependencies#169
deprecatedsupported@deprecated in the JSDoc comment
2020-12
prefixItemsignoredunknown[]; with items: falsenever[], with items: {…} that schema is applied to every element. Tuples are #816 (pending)#543
$dynamicRef / $dynamicAnchorignoredas $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

Who uses JSON-Schema-to-TypeScript?