A JSON Schema to Zod converter that works from a single JSON value emits reviewable Zod source code with no uploads and no runtime dependency, but it deliberately stops short of proving your real API contract because one sample cannot establish formats, ranges, enums, or business rules. The phrase "JSON Schema to Zod" gets used for two different jobs, and the distinction shapes what you can trust in the output. Some readers mean the JSON Schema specification — Draft 7, 2019-09, or 2020-12 — a vocabulary carrying type, properties, required, format, minimum, and pattern keywords. Other readers actually have one JSON value in hand and want Zod source they can paste into a TypeScript file. The JSON to Zod Schema Converter takes the second path: it parses one strict JSON value, walks the structure recursively, and emits a Zod declaration alongside an inferred TypeScript type. Knowing which job you have changes everything about which checks remain before the schema is safe to validate against production traffic.

JSON Schema Specification vs JSON Sample: Pick the Right Starting Point
If your input is a JSON Schema document — a literal object such as {"type":"object","properties":{"id":{"type":"string"}},"required":["id"]} — feeding it into a sample-based converter gives you a Zod schema that describes the schema object's own shape, not the data the schema was meant to validate. The output would treat type and properties as ordinary property names on a plain object, which is not useful. Translating real JSON Schema documents (Draft 7+) requires a different class of library that walks the keyword tree and emits corresponding Zod calls like z.object({ id: z.string() }) with .min(1) refinements for minLength and so on. Those libraries exist as npm packages; they are not what runs in your browser when you paste a value into a sample-first converter.
What the sample-first approach excels at is the much more common real-world task: you receive a single JSON payload from a teammate, an API response, or a saved fixture, and you want a starting Zod schema you can edit. The converter turns that one observation into importable source you can review, diff against documentation, and extend. That distinction — spec translation versus sample inference — is the first decision you should make before clicking generate.
Type Mapping the Generator Handles Automatically
The conversion is deterministic for primitives. Every observed JSON value falls into one of the documented Zod constructors below, and repeated conversions of the same JSON produce stable source because identical schemas are deduplicated and mixed values are ordered deterministically.
| Observed JSON value | Generated Zod expression | Notes |
|---|---|---|
| String such as "hello" | z.string() | No format inference; add .email(), .url(), or .uuid() manually from documentation. |
| Finite number such as 42 or 3.14 | z.number() | Already passed through JSON.parse; treat as JavaScript number precision. |
| Boolean true or false | z.boolean() | No coercion of truthy strings or 0/1 numbers. |
| null | z.null() | Combined with another observed type it becomes .nullable(). |
| Object {...} | z.object({...}) | Properties follow source encounter order, not alphabetical order. |
| Array [...] | z.array(...) | Empty arrays yield z.array(z.unknown()) rather than a guessed element type. |
Keys that are valid JavaScript identifiers stay unquoted in the output. Keys containing spaces, hyphens, numeric starts, or quotes are emitted with JSON string escaping so the surrounding z.object call remains valid source. This keeps the generated text readable while still handling API responses with unconventional property names.
Convert a JSON Value to Zod Source in Three Steps
The sample-first workflow is intentionally short. Each step has a review gate so you catch inference choices before they reach your codebase.
- Paste one valid and representative JSON value and enter a valid schema identifier such as UserSchema. Strict JSON only — JavaScript comments, trailing commas, single quotes, NaN, Infinity, undefined, and BigInt syntax are rejected rather than silently repaired. Use a single payload that represents the shape you actually want to validate, not a stripped-down example that hides optional fields.
- Generate the Zod source, then inspect the output for optional, nullable, union, numeric, and empty-array choices. Look for .optional() on properties that were missing from at least one object in the sample, look for .nullable() on keys that ever held null, and check that mixed-type arrays produced z.union([...]) entries in a stable order.
- Copy the source into a project with Zod installed and test it against the authoritative data contract. Run both accepted production cases and rejected boundary cases through the schema, not just the original sample, to confirm required fields, formats, and business rules all behave correctly.
The work is bounded at 500,000 input characters, 50,000 visited values, 40 nesting levels, and 1,000,000 generated characters. Hitting any of those limits returns an explicit error and never emits a partial schema. Deep or very wide examples can still be poor contract documentation even under those limits, so prefer a small representative fixture and split independent payloads into separate schemas.
Inference Decisions You Should Review Before Committing
Sample inference encodes a handful of deliberate choices that differ from how a human would write a schema. Understanding them upfront prevents surprises in code review.
| Situation in the sample | What the generator produces | Why |
|---|---|---|
| Empty array [] | z.array(z.unknown()) | No evidence of future element type; explicit uncertainty signal rather than a silent z.string() or z.never() guess. |
| Array of objects with overlapping keys | Merged keys, each value recursively merged | Captures the union of observed shapes across every element in the stated budget. |
| Object property present in every sample object | Required (no .optional()) | Consistent presence within the sample is treated as required. |
| Object property missing from at least one sample object | .optional() | Absence in any merged sample flips the property to optional. |
| Same key holding different types across samples | Recursive union in deterministic order | Preserves every observed value type without reordering between runs. |
| null combined with another observed type | z.string().nullable() style | Null is treated as a sentinel meaning "absent or unset" rather than a stand-alone type. |
The merge rules are deterministic, which means two people pasting the same JSON value on different machines will see byte-identical Zod source. That property matters for code review, diffs, and shared fixtures — repeated runs do not introduce noise, and identical schemas are deduplicated inside the same output.
What the Converter Refuses to Guess — and Why
The generator does not infer string formats, minimums, maximums, integer constraints, enums, literals, discriminated unions, dates, UUIDs, URLs, email addresses, brands, coercions, defaults, transformations, refinements, records, tuples, strict-object behavior, recursive references, or business rules. The reasoning is concrete: a single sample cannot distinguish "2024-03-15" from a generic string, cannot prove that a number field is always an integer, and cannot recover a closed enum from one observation. Spelling variations and small samples are not enough to establish intent, so the converter leaves those decisions to you.
Adding those rules by hand is straightforward once you have a stable starting schema. For example, after pasting a user object, you can extend id: z.string() with .min(1) if your contract guarantees a non-empty identifier, or with .uuid() if the API documents UUIDs. The official Zod API reference documents every method you can chain onto a base constructor, and pinning a specific Zod version in your package.json before running tests keeps the emitted source compatible with the runtime you actually deploy.
Numbers carry a specific caveat worth highlighting: because parsing happens through JSON.parse, every number in the sample already obeys JavaScript number precision. Identifiers, currency amounts, or any value that requires exact decimal semantics should usually remain strings — once the generator has produced z.number(), the schema will accept any number without checking for the precision your contract demands.
Wiring the Output Into a Real Project
The page that emits the schema does not load Zod, does not execute the generated code, and does not upload your sample. Installation happens in your own project, with a specific pinned version of Zod and a chosen module format. After copying the source, run a formatter such as Prettier, run the TypeScript compiler to confirm the inferred type lines up with your domain model, and write at least one test per accepted case and one per rejected case drawn from real fixtures rather than from the original sample.
Treat the generated schema as a starting point that needs review, not a finished contract. A field present in your one example may still be optional in production, a value absent from the sample may still be valid, and any constraint the API documents — ranges, enums, formats, required-on-create-but-optional-on-update semantics — has to be added by reading the authoritative documentation, not by inference. That review step is the difference between a schema that compiles and a schema that protects your application from bad data.