Free & open source — no account required
Free & open source — no account required
This technical guide provides an in-depth analysis of the typescript to json schema engine, best practices for implementation, and data security standards.
TypeScript's type system is a compile-time tool — at runtime, there's no mechanism to validate that an external API response or user submission actually matches your interface. JSON Schema fills this gap: it's a language-agnostic runtime validation specification that works in Node.js (ajv), browsers, Python (jsonschema), Go (gojsonschema), and CI assertion tools. Generating JSON Schema from TypeScript with ts-json-schema-generator keeps the TypeScript type as the single source of truth while producing a schema that can validate data in any environment. The critical techniques are: using JSDoc tags for constraints (@minimum, @pattern, @format), using $ref for recursive and shared types, and understanding the Draft 7 vs Draft 2020-12 differences that affect which validators you can use.
// Installation
// npm install --save-dev ts-json-schema-generator
// Basic usage: generate schema for a specific type
// npx ts-json-schema-generator --path src/types/product.ts --type Product --out schema/product.json
// tsconfig.json requirements for the generator:
{
"compilerOptions": {
"strictNullChecks": true, // required — controls nullable field generation
"esModuleInterop": true,
"lib": ["ES2020"]
}
}
// package.json script: regenerate all schemas on type changes
{
"scripts": {
"schema:generate": "ts-json-schema-generator --path 'src/types/*.ts' --type '*' --out schema/all.json",
"schema:check": "ajv validate -s schema/product.json -d test-fixtures/product.json"
}
}
// CI pipeline: fail if generated schema drifts from types
// Add to GitHub Actions:
// - run: npm run schema:generate
// - run: git diff --exit-code schema/ # fails if types changed without schema regen
// ts-json-schema-generator reads JSDoc tags and emits them as JSON Schema keywords.
// Without JSDoc tags, you only get type/required/properties — no format or range validation.
import { z } from 'zod'; // shown for comparison only
export interface Product {
/**
* Unique product identifier
* @minLength 5
* @maxLength 20
* @pattern ^[A-Z]{2}-\d{4,6}$
*/
sku: string;
/**
* Human-readable name
* @minLength 2
* @maxLength 100
*/
name: string;
/**
* Price in USD, stored as cents to avoid floating-point issues
* @minimum 0
* @maximum 1000000
* @multipleOf 1
*/
priceCents: number;
/**
* Discount percentage (0-100)
* @minimum 0
* @maximum 100
* @exclusiveMinimum 0
*/
discountPercent?: number;
/**
* ISO 8601 date when the product was added
* @format date-time
*/
createdAt: string;
/**
* Product page URL
* @format uri
*/
productUrl?: string;
/**
* Product image (base64 encoded)
* @contentEncoding base64
* @contentMediaType image/jpeg
*/
imageThumbnail?: string;
category: ProductCategory;
tags: string[];
}
export enum ProductCategory {
Electronics = 'electronics',
Apparel = 'apparel',
Home = 'home',
Sports = 'sports',
}
// Generated schema (excerpt):
// {
// "$schema": "http://json-schema.org/draft-07/schema#",
// "type": "object",
// "properties": {
// "sku": {
// "type": "string",
// "minLength": 5,
// "maxLength": 20,
// "pattern": "^[A-Z]{2}-\\d{4,6}$"
// },
// "priceCents": {
// "type": "integer",
// "minimum": 0,
// "maximum": 1000000,
// "multipleOf": 1
// },
// "createdAt": {
// "type": "string",
// "format": "date-time"
// },
// "category": {
// "type": "string",
// "enum": ["electronics", "apparel", "home", "sports"]
// }
// },
// "required": ["sku", "name", "priceCents", "createdAt", "category", "tags"]
// }
// Recursive types generate $defs (Draft 2020-12) or definitions (Draft 7)
// and use $ref to avoid infinite schema expansion
export interface TreeNode {
id: string;
label: string;
value?: number;
children: TreeNode[]; // recursive — would cause infinite JSON if inlined
}
// Generated schema handles this with $ref:
// {
// "$schema": "http://json-schema.org/draft-07/schema#",
// "definitions": {
// "TreeNode": {
// "type": "object",
// "properties": {
// "id": { "type": "string" },
// "label": { "type": "string" },
// "value": { "type": "number" },
// "children": { "type": "array", "items": { "$ref": "#/definitions/TreeNode" } }
// },
// "required": ["id", "label", "children"]
// }
// },
// "$ref": "#/definitions/TreeNode"
// }
// Shared types across multiple schemas: use $defs in a root schema
export interface ApiResponse<T> {
data: T;
meta: PaginationMeta;
errors: ApiError[];
}
export interface PaginationMeta {
/** @minimum 1 */
page: number;
/** @minimum 1 @maximum 100 */
perPage: number;
total: number;
}
export interface ApiError {
code: string;
message: string;
/**
* JSON Pointer to the field that caused the error
* @pattern ^(/[^/~]*(~[01][^/~]*)*)*$
*/
path?: string;
}
// Generate schemas for all types in a file:
// npx ts-json-schema-generator --path src/types/api.ts --type 'ApiResponse' --out schema/api-response.json
// Result uses $ref internally for PaginationMeta and ApiError
import Ajv, { ValidateFunction } from 'ajv';
import addFormats from 'ajv-formats'; // adds 'date-time', 'uri', 'email', etc.
import productSchema from '../schema/product.json';
import type { Product } from './types/product';
// ajv setup — Draft 7 (most compatible)
const ajv = new Ajv({ allErrors: true, strict: false });
addFormats(ajv); // required for @format date-time, uri, email
const validateProduct: ValidateFunction<Product> = ajv.compile(productSchema) as ValidateFunction<Product>;
function assertProduct(data: unknown): Product {
if (!validateProduct(data)) {
const errors = validateProduct.errors?.map(e =>
`${e.instancePath || '(root)'}: ${e.message}`
);
throw new Error(`Invalid product data:\n${errors?.join('\n')}`);
}
return data;
}
// Usage:
const raw = await fetchProductFromApi(id);
const product = assertProduct(raw);
// product is now typed as Product with validated constraints
// For Draft 2020-12 (newer, needed for if/then/else, unevaluatedProperties):
import Ajv2020 from 'ajv/dist/2020';
const ajv2020 = new Ajv2020({ allErrors: true });
// Note: ts-json-schema-generator defaults to Draft 7 output
// Use --schema-id flag to switch: --schema-id http://json-schema.org/draft/2020-12/schema
// Draft 7 (default output): compatible with ajv v8, most online validators,
// OpenAPI 3.0 (which embeds Draft 4/7 schemas)
// Draft 2020-12: required for:
// - unevaluatedProperties (stricter object validation than additionalProperties)
// - $dynamicRef/$dynamicAnchor (improved recursive references)
// - prefixItems (for typed tuple validation — replaced Draft 7's items array form)
// - if/then/else (available in Draft 7, but 2020-12 has improved anchoring)
// Draft 7 tuple (deprecated in 2020-12):
{
"type": "array",
"items": [{ "type": "string" }, { "type": "number" }], // tuple form
"additionalItems": false
}
// Draft 2020-12 tuple:
{
"type": "array",
"prefixItems": [{ "type": "string" }, { "type": "number" }],
"items": false // no additional items
}
// Recommendation:
// - Use Draft 7 for: REST API validation, OpenAPI integration, widest tool support
// - Use Draft 2020-12 for: new projects with ajv 2020, strict object validation
// with unevaluatedProperties, schema registries requiring latest spec
// Schema registry hosting (share schemas between services):
// POST your schema JSON to a registry URL so other services can reference it
// OpenAPI schema: $ref: 'https://schemas.yourcompany.com/product.json#'
// ajv remote ref: ajv.addSchema(schema, 'https://schemas.yourcompany.com/product.json')
additionalProperties: false to catch unknown fields: By default, JSON Schema (and ts-json-schema-generator output) allows extra properties — your TypeScript interface might not have them, but the schema won't reject them. Pass --additional-properties false to the generator, or add it manually to the root schema. This catches API responses that add new fields before your types are updated, preventing silent data corruption.ts-json-schema-generator in CI and git diff --exit-code schema/ to fail the build if TypeScript types changed without regenerating schemas. This prevents the schemas used by Python/Go services from drifting from the TypeScript source of truth. The generation is fast enough to run on every PR.ajv-formats for @format tags to work at runtime: ajv v8 does not validate format keywords by default — it requires the ajv-formats package. Without it, @format date-time annotations are silently ignored and invalid dates like "not-a-date" pass validation. Always install and register ajv-formats alongside ajv.Q: What is the difference between ts-json-schema-generator and typescript-json-schema?
A: Both use the TypeScript compiler API. ts-json-schema-generator (the newer package) is actively maintained, supports TypeScript 5.x, handles generics more correctly, and has better JSDoc annotation support. The older typescript-json-schema package has known issues with complex generics, mapped types, and conditional types. Prefer ts-json-schema-generator for new projects.
Q: How do I generate JSON Schema for generic types like ApiResponse<Product>?
A: ts-json-schema-generator partially supports generics. For instantiated generics, create a named alias: export type ProductApiResponse = ApiResponse<Product> and generate for ProductApiResponse. The generator will inline the generic parameter. Fully dynamic generics (where T is unknown at generation time) produce incomplete schemas with {} for the type parameter — create concrete aliases for each variant you need.
Q: Can I use Zod instead of generating JSON Schema from TypeScript?
A: Yes — Zod and ts-json-schema-generator solve the same problem from different directions. Zod defines validation logic first and can export JSON Schema with zod-to-json-schema. ts-json-schema-generator starts from TypeScript interface definitions. Use Zod when validation logic is complex (cross-field validation, transforms, async refinements) and you want to keep types and validation colocated. Use ts-json-schema-generator when you have existing TypeScript interfaces you don't want to rewrite in Zod, or when you need JSON Schema output to share with non-TypeScript teams.
Is the processing local-only?
Absolutely. TypeMorph operates entirely within your browser's sandbox. We use Web Workers for high-performance computation without ever transmitting your JSON, SQL, or API data to a remote server.
Can I use this for enterprise projects?
Yes. The tool is designed for professional software engineers who require GDPR compliance and data privacy. It is trusted by developers at top-tier startups and financial institutions.
Why pasting proprietary company data into third-party web tools is a major liability, and how to stay safe.
A deep dive into combining Zod, React Query, and TypeScript for bulletproof API integration.
Code generation is just the beginning. Discover how a schema-first approach can eliminate 90% of your integration bugs.