Generating JSON Schema Automatically from TypeScript Types
AI generated
type
TypeScript · JSON Schema · API contracts
Generating JSON Schema from TypeScript types
A single source of truth for types, validation and API documentation instead of duplicate maintenance

Teams that maintain TypeScript interfaces and JSON Schema separately eventually lose sync between the two: a field gets renamed in the type, the schema keeps the old name, and a consumer validates against rules that no longer match the real API. Tools like ts-json-schema-generator and zod-to-json-schema derive the schema directly from TypeScript code, making the type the single source of truth.

9 min read JSON Schema Validation API contracts

1. The synchronization problem between types and schema

In many projects, TypeScript interfaces exist for internal use alongside a separate JSON Schema for external API contracts, request validation, or OpenAPI documentation, often hand-maintained in two different files. As soon as a field is added or a type changes, that change has to be replicated in both places, and in practice that step is regularly forgotten.

The result is a schema that lags behind the actual code: an optional field becomes required in the TypeScript type, but the JSON Schema still marks it optional, and a client relying on the schema sends requests that fail at a point that should really have been caught at build time.

Automatic generation flips this relationship: the TypeScript type becomes the single source from which both the compile-time type and the runtime schema are derived, either by analyzing the TypeScript AST or through a library that produces schema and type together from one definition.

2. ts-json-schema-generator: deriving schema directly from interfaces

ts-json-schema-generator analyzes a project's TypeScript AST and produces a complete JSON Schema from a named interface or type alias, including nested types, unions, enums, and JSDoc comments that end up as the description field in the schema. The advantage over hand-written interfaces plus a separate schema: there is only one definition, and the schema is always a pure derivation.

The library can be used both from the CLI and programmatically through the Node API, which fits build pipelines where the schema is automatically regenerated on every build and, for example, stored as a static file for API documentation.


// types/order.ts
/** An order in the system. */
export interface Order {
  /** Unique order ID, UUID v4 */
  id: string;
  /** Total amount in cents, always positive */
  totalCents: number;
  status: "pending" | "shipped" | "cancelled";
  items: OrderItem[];
}

export interface OrderItem {
  sku: string;
  quantity: number;
}

3. Wiring generation into the build pipeline via CLI

The CLI call needs the path to the type file, the name of the root type, and optionally a tsconfig path for path aliases or strict compiler options. The generated schema can be written directly into a schemas/ directory and consumed there by validation libraries like AJV.

In CI pipelines an additional diff check pays off: the schema is generated and compared against the checked-in version, and a mismatch fails the build. This ensures nobody changes a type without also checking in the generated schema, which reviewers can then see directly.


# Generate schema for the Order type
npx ts-json-schema-generator \
  --path types/order.ts \
  --type Order \
  --tsconfig tsconfig.json \
  --out schemas/order.schema.json

# In CI: detect drift between code and the checked-in schema
npx ts-json-schema-generator --path types/order.ts --type Order \
  | diff - schemas/order.schema.json || \
  (echo "Schema is stale, please regenerate" && exit 1)

4. The reverse path: zod schema as the starting point

An alternative approach flips the order: instead of deriving a schema from TypeScript types, a zod schema is defined first, from which both the TypeScript type via z.infer and the JSON Schema via zod-to-json-schema are derived. This path has the advantage that the zod schema can simultaneously be used for runtime validation, something pure AST-based generation does not provide.

For new projects that need runtime validation anyway, the zod-first approach is usually the more pragmatic choice, because it produces type, validation and schema from a single definition. For existing codebases with established interfaces, ts-json-schema-generator is often the path with less rework.


import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";

const OrderSchema = z.object({
  id: z.string().uuid(),
  totalCents: z.number().int().positive(),
  status: z.enum(["pending", "shipped", "cancelled"]),
  items: z.array(
    z.object({ sku: z.string(), quantity: z.number().int().positive() })
  ),
});

type Order = z.infer<typeof OrderSchema>;

const jsonSchema = zodToJsonSchema(OrderSchema, "Order");
// jsonSchema is a complete JSON Schema document

5. Edge cases: mapped types, generics and discriminated unions

Not every TypeScript type translates losslessly into JSON Schema. Mapped types like Record<string, T> typically become additionalProperties definitions, which is semantically similar but not identical. Generics require the generator to either receive a concrete instantiation of the generic type or have the generic resolved explicitly before generation runs.

Discriminated unions with a shared literal field like type: "a" | "b" map well to oneOf with matching const values, and both ts-json-schema-generator and zod-to-json-schema support this pattern reliably as long as the discriminator values are distinct string literals.


type PaymentEvent =
  | { type: "charge"; amountCents: number }
  | { type: "refund"; amountCents: number; reason: string };

// Becomes a oneOf with a const discriminator in JSON Schema:
// { "oneOf": [ { "properties": { "type": { "const": "charge" }, ... } }, ... ] }

6. Validating the generated schema at runtime with AJV

The generated JSON Schema only earns its keep once it is actually used at runtime, for instance to validate incoming request bodies before they are passed into typed code. AJV compiles the schema into a fast validation function and returns a structured list of violated rules on failure.

It matters to run AJV with strict: true and consistently reject unknown extra fields in requests, otherwise fields that were never part of the type quietly slip through validation unnoticed.


import Ajv from "ajv";
import orderSchema from "../schemas/order.schema.json";

const ajv = new Ajv({ strict: true, allErrors: true });
const validateOrder = ajv.compile(orderSchema);

function parseOrder(raw: unknown): Order {
  if (!validateOrder(raw)) {
    throw new Error(ajv.errorsText(validateOrder.errors));
  }
  return raw as Order;
}

7. Embedding generated schemas into OpenAPI documents

Because OpenAPI 3.1 uses a full subset of JSON Schema for schema objects, generated schemas can be embedded directly under components.schemas in an OpenAPI specification without manual translation. That means an automatically generated API documentation always matches the actual TypeScript types exactly, without anyone maintaining the docs separately.

For older OpenAPI versions like 3.0, which are not fully JSON-Schema-compatible, an extra translation step is needed, for example turning const keywords into a single-value enum, something many tools in this ecosystem now handle automatically.

8. A recommended CI workflow for generated schemas

A robust workflow generates the schema as part of the build script, checks the generated file into the repository, and lets a CI step verify the checked-in version still matches the current type. This combines the traceability of a versioned file with the guarantee that it never goes stale.

Alternatively, some teams generate the schema entirely at build time without checking it in, which avoids repository noise but removes reviewers' ability to see changes to the public API contract directly in the pull request diff, a trade-off each team has to weigh for itself.

9. When automatic schema generation pays off

For internal types that never leave the Node process boundary, a generated JSON Schema is usually unnecessary overhead. But once a type serves as an API contract with external consumers, as a validation rule for incoming requests, or as the basis for OpenAPI documentation, automatic derivation quickly pays for itself, because it makes drift between type and schema structurally impossible.

The choice between AST-based generation and the zod-first approach mainly depends on whether runtime validation already exists in the project: where it is already present, zod is usually the more natural starting point, where plain type definitions dominate, ts-json-schema-generator is the path with the least rework.

Feature ts-json-schema-generator zod-to-json-schema Manual schema
Starting point existing TS types/interfaces zod schema definition separate JSON file
Runtime validation included no, schema only yes, directly via zod no
JSDoc comments carried over yes, as description partially via .describe() manual
Effort with existing code low medium, migration to zod needed high, ongoing dual maintenance
Discriminated unions as oneOf with const as oneOf with literal manually rebuilt

Mironsoft

TypeScript migration, type safety, and team onboarding

A JavaScript codebase without type safety, but no time for a full migration?

We migrate existing JavaScript projects to TypeScript step by step, set up strict compiler settings cleanly, and bring teams to the same type-safety level with code reviews and style guides.

Migration Roadmap

Plan and execute a gradual JS-to-TS migration without big-bang risk.

Strict Mode Rollout

Set up tsconfig.json, ESLint rules, and CI checks for lasting type safety.

Team Onboarding

Bring developers up to speed on TypeScript best practices with workshops and reviews.

10. Summary

JSON Schema generation

AST approach

ts-json-schema-generator derives schema from existing interfaces

zod approach

One schema produces type, validation and JSON Schema at once

Runtime check

AJV compiles the generated schema into a fast validation function

CI safeguard

A diff check prevents stale, checked-in schema files

11. FAQ: JSON Schema generation

1Do I need to switch my whole project to zod to generate JSON Schema?
No, ts-json-schema-generator works directly with existing TypeScript interfaces and type aliases, with no zod involved. Switching to zod only pays off if runtime validation is needed anyway.
2Are JSDoc comments carried over into the generated schema?
With ts-json-schema-generator yes, JSDoc descriptions above a field automatically land in the description attribute of the generated schema, keeping documentation right at the type.
3How do I handle generic types that need to be generated?
The generator needs either a concrete instantiation of the generic, such as Response, or the generic resolved into a concrete type alias beforehand, before generation runs.
4Can I translate discriminated unions losslessly into JSON Schema?
Yes, as long as the discriminator is a distinct string literal, the union is reliably mapped as a oneOf with matching const values per variant.
5Is generated JSON Schema compatible with OpenAPI 3.1?
Yes, OpenAPI 3.1 uses JSON Schema directly as its schema format, so generated schemas can be embedded under components.schemas without translation.
6What happens with older OpenAPI versions like 3.0?
An extra translation step is needed there, because 3.0 is not fully JSON-Schema-compatible, for example const keywords need converting into a single-value enum.
7Should the generated schema be checked into the repository?
Usually yes, with a CI check detecting drift between code and the checked-in schema, since reviewers can then see changes to the API contract directly in the pull request diff.
8How fast is AJV compared to manual validation?
AJV compiles schemas into optimized JavaScript functions and is among the fastest validation libraries in the Node ecosystem, considerably faster than generic, interpreted validation approaches.
9Can I generate multiple types in a single schema run?
Yes, both ts-json-schema-generator and zod-to-json-schema support multiple root types, either as separate files or as one schema with several definitions entries.
10Is this worth setting up for a small internal tool?
Rarely, for a small tool with no external API consumers a plain TypeScript interface without a generated schema is usually enough, the extra effort pays off once real API contracts are involved.