json-schema-to-ts
json-schema-to-ts is a single TypeScript type, FromSchema, that reads a JSON Schema written as a TypeScript literal and produces the matching static type. You write the schema once, add as const so TypeScript keeps the literal values instead of widening them to string and boolean, and then type Dog = FromSchema<typeof dogSchema> gives you the object type with the right required and optional keys, enum unions, tuple shapes, and nullability. It does the work entirely in type space during compilation, so nothing runs at runtime and nothing ends up in your bundle. It does not validate data. The usual pairing is Ajv or Fastify doing runtime validation against the same schema object, with FromSchema supplying the static half so the two can never drift apart.
If JSON Schema is imposed on you by an API contract and you are tired of keeping a parallel interface in sync, FromSchema solves that with no runtime footprint and is worth the as const discipline. If you get to choose the format, TypeBox or Zod give you validation and types from one definition and are under active development, which this project currently is not.
Use it if
- JSON Schema is already your contract because of OpenAPI, AWS API Gateway request models, Fastify route schemas, or a spec shared with services that are not written in TypeScript
- You are maintaining a schema and a hand-written interface side by side today, and they have already disagreed at least once
- Runtime validation is handled: Ajv, Fastify, or a gateway is checking payloads, and the only thing missing is the compile-time type
- You want zero runtime cost. FromSchema is a type-only import, so nothing survives compilation and no bundle grows
- You want the compiler to catch bad schemas: an allOf that no value can satisfy resolves to never, and a misplaced additionalItems shows up as a wrong inferred type right in your editor
- You need runtime validation too. This library gives you types only, so you still install and configure Ajv. Zod or TypeBox give you one object that both validates and types, which is less machinery overall
- Your schemas are recursive. A self-reference such as items: { $ref: '#' } does not resolve; TypeScript reports TS2615, type of property circularly references itself in mapped type, and the README confirms recursive schemas are unsupported
- Your schemas live in .json files. TypeScript widens imported JSON, so you cannot apply as const to it, and the project has a dedicated FAQ entry saying so. Schemas have to be TypeScript literals
- Editor responsiveness matters and your schemas are large. Everything is computed by the type checker, which is why parseNotKeyword and parseIfThenElseKeywords are opt-in, and why the FAQ has an entry for the excessively deep type instantiation error
- You rely on oneOf meaning exactly one. FromSchema parses it identically to anyOf, so an object matching two branches still type checks; the README shows an invalid value that raises no error
- You want an actively released dependency. The last publish is 3.1.1 from August 2024 and the default branch's newest commit is a typo fix from October 2025, with the later repo activity being dependabot branches. It works, but do not expect fixes
Setup reality
npm install --save-dev json-schema-to-ts, and because everything is type-level you can import type { FromSchema } and never pull the package into your runtime graph. The friction is entirely in how you write schemas. Every schema needs as const or FromSchema resolves to a useless widened type, and that includes nested schemas you compose in. Since TypeScript 4.9 the better form is as const satisfies JSONSchema, which gets you autocomplete and catches schema mistakes at the definition site instead of at the FromSchema call. strict mode is required. The README also tells you to disable noStrictGenericChecks, which is worth knowing is no longer a valid compiler option in TypeScript 7, so that instruction only applies to 4.x through 6.x toolchains. Minimum supported TypeScript is 4.3 and Node 16. The package declares @babel/runtime and ts-algebra as dependencies even though a type-only import uses neither.
Patterns
Turn a schema into a TypeScript typeinfer-type-from-schema
import type { FromSchema } from 'json-schema-to-ts';
const dogSchema = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'integer' },
favoriteFood: { enum: ['pizza', 'taco', 'fries'] },
},
required: ['name', 'age'],
additionalProperties: false,
} as const;
type Dog = FromSchema<typeof dogSchema>;
// { name: string; age: number; favoriteFood?: 'pizza' | 'taco' | 'fries' }Drop the as const and TypeScript widens 'object' to string, at which point FromSchema cannot tell anything about the schema and the result is useless. This is the single most common mistake with this library.
Type check the schema itself while writing itsatisfies-json-schema
import type { FromSchema, JSONSchema } from 'json-schema-to-ts';
const dogSchema = {
type: 'object',
properties: { name: { type: 'string' } },
required: ['name'],
additionalProperties: false,
} as const satisfies JSONSchema;
type Dog = FromSchema<typeof dogSchema>;as const satisfies JSONSchema needs TypeScript 4.9 or newer and is the form to prefer: you get autocomplete inside the schema and a typo like additionalItems on an object errors where you wrote it, not two files away.
Control whether extra properties are allowedopen-vs-closed-objects
const openSchema = {
type: 'object',
properties: { foo: { type: 'string' } },
required: ['foo'],
} as const;
type Open = FromSchema<typeof openSchema>;
// { [x: string]: unknown; foo: string }
const closedSchema = { ...openSchema, additionalProperties: false } as const;
type Closed = FromSchema<typeof closedSchema>;
// { foo: string }JSON Schema objects are open by default, so without additionalProperties: false you get an index signature of unknown and destructuring unknown keys compiles fine. Add it wherever you want the type to be exact.
Reference definitions inside the same schemashared-definitions
const userSchema = {
type: 'object',
properties: {
name: { $ref: '#/definitions/name' },
age: { $ref: '#/definitions/age' },
},
required: ['name', 'age'],
additionalProperties: false,
definitions: {
name: { type: 'string' },
age: { type: 'integer' },
},
} as const;
type User = FromSchema<typeof userSchema>;
// { name: string; age: number }Internal $ref works only for non-recursive shapes. Point a definition back at itself, or use $ref: '#' for a tree node, and TypeScript raises TS2615 about a property circularly referencing itself in a mapped type.
Resolve $ref across separate schema objectsexternal-references
const userSchema = {
$id: 'http://example.com/schemas/user.json',
type: 'object',
properties: { name: { type: 'string' } },
required: ['name'],
additionalProperties: false,
} as const;
const usersSchema = {
type: 'array',
items: { $ref: 'http://example.com/schemas/user.json' },
} as const;
type Users = FromSchema<typeof usersSchema, { references: [typeof userSchema] }>;Types cannot hold a registry the way an Ajv instance does, so every referenced schema has to be listed in the references tuple by hand. Forget one and the $ref resolves to unknown rather than erroring.
Map string formats onto richer runtime typesdeserialize-formats
type Email = string & { brand: 'email' };
type User = FromSchema<
typeof userSchema,
{
deserialize: [
{ pattern: { type: 'string'; format: 'email' }; output: Email },
{ pattern: { type: 'string'; format: 'date-time' }; output: Date },
];
}
>;
// birthDate is Date, email is EmailThis only changes the static type. Nothing converts the JSON string into a Date at runtime, so pair it with a validator or parser that actually performs the conversion or the type is a lie.
Wrap Ajv so validation narrows the typetypeguard-with-ajv
import Ajv from 'ajv';
import { wrapCompilerAsTypeGuard } from 'json-schema-to-ts';
import type { $Compiler } from 'json-schema-to-ts';
const ajv = new Ajv();
const $compile: $Compiler = (schema) => ajv.compile(schema);
const compile = wrapCompilerAsTypeGuard($compile);
const isPet = compile(petSchema);
if (isPet(data)) {
data.name; // narrowed to the inferred Pet type
}wrapCompilerAsTypeGuard and wrapValidatorAsTypeGuard are the only runtime exports in the package (alongside asConst); they just return the function you passed in with a type guard signature attached.
Parse the not keyword to narrow enumsopt-in-not-keyword
const petSchema = {
type: 'object',
properties: { animal: { enum: ['cat', 'dog', 'boat'] } },
not: { properties: { animal: { const: 'boat' } } },
required: ['animal'],
additionalProperties: false,
} as const;
type Pet = FromSchema<typeof petSchema, { parseNotKeyword: true }>;
// { animal: 'cat' | 'dog' }Off by default because the exclusion computation is expensive enough that TypeScript sometimes gives up and returns any. It also only propagates when the exclusion collapses onto a single property; anything wider is silently ignored.
Turn conditional schemas into a discriminated unionif-then-else
type Pet = FromSchema<
typeof petSchema,
{ parseIfThenElseKeywords: true }
>;
// { animal: 'dog'; dogBreed: DogBreed; catBreed?: CatBreed }
// | { animal: 'cat'; catBreed: CatBreed; dogBreed?: DogBreed }Computed as (If and Then) or (not If and Else), so it inherits every limitation of the not keyword. On a schema with several conditions this is where compile times start to hurt.
Stop defaulted properties from becoming requireddefaults-stay-optional
const schema = {
type: 'object',
properties: { foo: { type: 'string', default: 'bar' } },
additionalProperties: false,
} as const;
type A = FromSchema<typeof schema>;
// { foo: string }
type B = FromSchema<typeof schema, { keepDefaultedPropertiesOptional: true }>;
// { foo?: string }The default assumes your validator fills defaults in (Ajv with useDefaults). If it does not, the type promises a value that will not be there at runtime, so set the option when you are only validating and not coercing.
Add custom keywords to your schemasextend-the-spec
import type { ExtendedJSONSchema, FromExtendedSchema } from 'json-schema-to-ts';
type CustomProps = { numberType: 'int' | 'float' | 'bigInt' };
const bigIntSchema = {
type: 'number',
numberType: 'bigInt',
} as const satisfies ExtendedJSONSchema<CustomProps>;
type Big = FromExtendedSchema<
CustomProps,
typeof bigIntSchema,
{ deserialize: [{ pattern: { type: 'number'; numberType: 'bigInt' }; output: bigint }] }
>;Useful when your gateway or ORM reads extra keywords that plain JSONSchema rejects. Remember the runtime validator still needs those keywords registered separately or it will fail on them in strict mode.
Narrow a schema without writing as constas-const-helper
import { asConst } from 'json-schema-to-ts';
import type { FromSchema } from 'json-schema-to-ts';
const dogSchema = asConst({
type: 'object',
properties: { name: { type: 'string' } },
required: ['name'],
});
type Dog = FromSchema<typeof dogSchema>;asConst is a real function call, so unlike as const it survives into the compiled output and makes the package a runtime dependency. Prefer as const satisfies JSONSchema unless you are writing JavaScript with JSDoc types.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @sinclair/typebox | npm | You want one builder that emits a real JSON Schema object and the TypeScript type together, plus a fast validator |
| zod | npm | JSON Schema is not a hard requirement and you would rather define schemas in TypeScript and get validation and types from one call |
| ajv | npm | You need the runtime validation half; json-schema-to-ts deliberately does not validate anything |