Free & open source — no account required

v1.2.5-PRICING-19
Web & Frontend • Engineering Documentation

TypeScript to JSON Schema Converter

This technical guide provides an in-depth analysis of the typescript to json schema engine, best practices for implementation, and data security standards.

TypeScript to JSON Schema: ts-json-schema-generator, JSDoc Constraints, $ref for Recursive Types, and Draft 2020-12 vs Draft 7

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.

CLI Setup: ts-json-schema-generator

// 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

JSDoc Tags for Validation Constraints

// 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"]
// }

$ref for Recursive Types and Shared Definitions

// 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

Runtime Validation with ajv

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 vs Draft 2020-12: Key Differences

// 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')

Best Practices for Production

  • Add 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.
  • Commit generated schemas to version control and check for drift in CI: Run 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.
  • Use 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.
  • Generate schemas at build time, not at startup: Running ts-json-schema-generator at application startup is slow (it invokes the TypeScript compiler) and requires devDependencies in production. Run it as a build step, commit the output JSON files, and import them as static JSON in your production code. The schemas are stable artifacts, not dynamic outputs.

FAQ

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.

Developer FAQ

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.