JSON to Zod Schema — JavaScript 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 JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
/**
* json-to-zod - JavaScript polyglot showcase port.
*
* Recursively infers a Zod schema string from a JSON value. Mixed-type arrays
* collapse to z.union(...); plain objects become z.object({...}); empty arrays
* and objects fall back to z.array(z.unknown()) / z.object({}). The function
* never throws - JSON parse failures and inference problems are surfaced as
* { ok: false, error }.
*
* Ported from src/lib/jsonToZod.ts (CosmoDev).
* Display source - part of CosmoDev's polyglot tool pages (dev.cosmolabs.org).
*/
/**
* A "plain object" is a non-null, non-array object - i.e. exactly what
* JSON.parse produces for `{...}`. We exclude arrays explicitly because
* typeof [] === 'object'.
*/
function isPlainObject(v) {
return typeof v === 'object' && v !== null && !Array.isArray(v);
}
/**
* Turn an arbitrary string into a valid JS identifier:
* - drop every char outside [A-Za-z0-9_$]
* - replace each leading digit with '_' (identifiers can't start with a digit)
* - default to "schema" if nothing usable remains
*/
function sanitizeVarName(name) {
const cleaned = name
.replace(/[^A-Za-z0-9_$]/g, '')
.replace(/^[0-9]+/, (m) => '_'.repeat(m.length));
return cleaned || 'schema';
}
/**
* Indent every non-empty line of `s` by `depth` spaces. Empty lines are left
* untouched so blank separators don't acquire trailing whitespace.
*/
function pad(s, depth) {
const p = ' '.repeat(depth);
return s
.split('\n')
.map((l) => (l.length ? p + l : l))
.join('\n');
}
/**
* Infer a Zod schema string for `value` at the given indentation depth.
* Exported so callers can convert an already-parsed value directly.
*/
export function inferZod(value, indent = 2) {
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)) {
// Empty array → unknown element type (Zod has nothing to infer from).
if (value.length === 0) return 'z.array(z.unknown())';
// Recurse at a deeper indent so nested shapes line up.
const types = value.map((e) => inferZod(e, indent + 2));
const distinct = [...new Set(types)];
// Single shared element type → z.array(T). Multiple → z.union([...]).
// Note: the union intentionally renders the *full* `types` list (with
// duplicates), matching the TypeScript reference exactly.
let inner;
if (distinct.length === 1) {
inner = distinct[0];
} else {
inner = `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}})`;
}
// Safety net: for valid JSON this is unreachable (JSON has no undefined /
// bigint / function), but we keep a deterministic fallback.
return 'z.unknown()';
}
/**
* Convert a JSON string into a `const NAME = <zod schema>;` declaration.
*
* @param {string} jsonString - Raw JSON input.
* @param {{ rootName?: string }} [opts] - Optional output variable name.
* @returns {{ ok: boolean, code: string, error: string|null }}
*/
export function jsonToZod(jsonString, opts = {}) {
let value;
try {
value = JSON.parse(jsonString);
} catch (e) {
return { ok: false, code: '', error: e?.message ?? String(e) };
}
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?.message ?? String(e) };
}
}
export default jsonToZod;
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 →