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.
Table of Contents
- 1. The synchronization problem between types and schema
- 2. ts-json-schema-generator: deriving schema directly from interfaces
- 3. Wiring generation into the build pipeline via CLI
- 4. The reverse path: zod schema as the starting point
- 5. Edge cases: mapped types, generics and discriminated unions
- 6. Validating the generated schema at runtime with AJV
- 7. Embedding generated schemas into OpenAPI documents
- 8. A recommended CI workflow for generated schemas
- 9. When automatic schema generation pays off
- 10. Summary
- 11. FAQ
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