JSON to Zod Schema — Python 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 Python implementation — the same logic the interactive tool runs, in a shareable, citable form.
"""
json-to-zod — Python 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 raises — JSON parse failures and inference problems are returned as
{"ok": False, "error": str}.
Ported from src/lib/jsonToZod.ts (CosmoDev).
Display source — part of CosmoDev's polyglot tool pages (dev.cosmolabs.org).
"""
from __future__ import annotations
import json
import re
from typing import Any
# Drop every character that is not a valid identifier byte, then turn leading
# digits into underscores (a JS identifier cannot start with a digit).
_INVALID_VAR_CHARS = re.compile(r"[^A-Za-z0-9_$]")
_LEADING_DIGITS = re.compile(r"^[0-9]+")
def _sanitize_var_name(name: str) -> str:
"""Reduce an arbitrary string to a usable JS variable name."""
cleaned = _INVALID_VAR_CHARS.sub("", name)
cleaned = _LEADING_DIGITS.sub(lambda m: "_" * len(m.group()), cleaned)
return cleaned or "schema"
def _pad(s: str, depth: int) -> str:
"""Indent every non-empty line by ``depth`` spaces; leave blanks alone."""
prefix = " " * depth
return "\n".join((prefix + line) if line else line for line in s.split("\n"))
def _distinct(seq: list[str]) -> list[str]:
"""Order-preserving de-duplication, mirroring JS ``[...new Set(seq)]``."""
seen: set[str] = set()
out: list[str] = []
for item in seq:
if item not in seen:
seen.add(item)
out.append(item)
return out
def infer_zod(value: Any, indent: int = 2) -> str:
"""Infer a Zod schema string for ``value`` at the given indentation depth."""
if value is None:
return "z.null()"
# bool MUST be checked before int: in Python ``isinstance(True, int)`` is
# True, whereas JSON treats booleans and numbers as distinct types.
if isinstance(value, bool):
return "z.boolean()"
if isinstance(value, (int, float)):
return "z.number()"
if isinstance(value, str):
return "z.string()"
if isinstance(value, list):
if not value:
return "z.array(z.unknown())"
types = [_infer_zod(e, indent + 2) for e in value]
distinct = _distinct(types)
# Single shared element type → z.array(T). Otherwise z.union([...]).
# The union renders the *full* ``types`` list (duplicates included) to
# match the TypeScript reference byte-for-byte.
if len(distinct) == 1:
inner = distinct[0]
else:
inner = (
"z.union([\n"
+ _pad(",\n".join(types), indent + 2)
+ "\n"
+ _pad("", indent)
+ "])"
)
return f"z.array({inner})"
if isinstance(value, dict):
pad0 = " " * indent
pad1 = " " * (indent + 2)
# dict preserves insertion order in CPython 3.7+, so emitted fields
# follow source order for the (common) case of string keys.
if not value:
return "z.object({})"
fields = "\n".join(
f"{pad1}{key}: {_infer_zod(v, indent + 2)},"
for key, v in value.items()
)
return f"z.object({{\n{fields}\n{pad0}}})"
# Unreachable for valid JSON, kept as a deterministic safety net.
return "z.unknown()"
# Internal alias so the recursive calls above resolve even if a user shadows
# the public name via ``from module import infer_zod as ...``.
_infer_zod = infer_zod
def json_to_zod(json_string: str, root_name: str | None = None) -> dict[str, Any]:
"""
Convert a JSON string into a ``const NAME = <zod schema>;`` declaration.
Returns a dict with ``ok``, ``code`` and ``error`` keys — never raises.
"""
try:
value = json.loads(json_string)
except json.JSONDecodeError as exc:
return {"ok": False, "code": "", "error": str(exc)}
try:
name = _sanitize_var_name(root_name or "Root")
return {
"ok": True,
"code": f"const {name} = {_infer_zod(value, 2)};",
"error": None,
}
except Exception as exc: # pragma: no cover - defensive, mirrors TS try/catch
return {"ok": False, "code": "", "error": str(exc)}
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 →