Skip to content

JSON to Zod Schema — TypeScript source

Generate Zod validation schemas from JSON. Infers z.string, z.number, z.boolean, z.object, z.array, z.null, and z.union for mixed arrays.

This is the TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Pure JSON → Zod converter - no React, no DOM, deterministic.
// Recursively infers z.* validators; mixed arrays become z.union. Never throws.

export interface Options {
  rootName?: string;
}

export interface Result {
  ok: boolean;
  code: string;
  error: string | null;
}

function isPlainObject(v: unknown): v is Record<string, unknown> {
  return typeof v === 'object' && v !== null && !Array.isArray(v);
}

function sanitizeVarName(name: string): string {
  const cleaned = name.replace(/[^A-Za-z0-9_$]/g, '').replace(/^[0-9]+/, (m) => '_'.repeat(m.length));
  return cleaned || 'schema';
}

export function jsonToZod(jsonString: string, opts: Options = {}): Result {
  let value: unknown;
  try {
    value = JSON.parse(jsonString);
  } catch (e) {
    return { ok: false, code: '', error: (e as Error).message };
  }
  try {
    const rootName = sanitizeVarName(opts.rootName || 'Root');
    return { ok: true, code: `const ${rootName} = ${inferZod(value, 2)};`, error: null };
  } catch (e) {
    return { ok: false, code: '', error: (e as Error).message };
  }
}

/** Infer a Zod schema string for a value. indent = current indentation depth. */
export function inferZod(value: unknown, indent = 2): string {
  if (value === null) return 'z.null()';
  switch (typeof value) {
    case 'string':
      return 'z.string()';
    case 'number':
      return 'z.number()';
    case 'boolean':
      return 'z.boolean()';
  }
  if (Array.isArray(value)) {
    if (value.length === 0) return 'z.array(z.unknown())';
    const types = value.map((e) => inferZod(e, indent + 2));
    const distinct = [...new Set(types)];
    const inner = distinct.length === 1 ? distinct[0] : `z.union([\n${pad(types.join(',\n'), indent + 2)}\n${pad('', indent)}])`;
    return `z.array(${inner})`;
  }
  if (isPlainObject(value)) {
    const pad0 = ' '.repeat(indent);
    const pad1 = ' '.repeat(indent + 2);
    const entries = Object.entries(value);
    if (entries.length === 0) return 'z.object({})';
    const fields = entries.map(([k, v]) => `${pad1}${k}: ${inferZod(v, indent + 2)},`);
    return `z.object({\n${fields.join('\n')}\n${pad0}})`;
  }
  return 'z.unknown()';
}

function pad(s: string, depth: number): string {
  const p = ' '.repeat(depth);
  return s
    .split('\n')
    .map((l) => (l.length ? p + l : l))
    .join('\n');
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →