Free & open source — no account required

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

Zod to JSON Schema Generator

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

Zod to JSON Schema: Make Your Validators Interoperable

Zod is a TypeScript-runtime validator — its schemas live in your code and can't be read by tools that speak the cross-language standard, JSON Schema. Converting Zod to JSON Schema unlocks the rest of the ecosystem: OpenAPI documents, form generators, contract tests, validators in other languages, and LLM structured outputs. This converter turns a Zod schema into a JSON Schema document you can hand to any of them. It's a faithful, mechanical translation you can read line by line — a starting point you keep refining, not a black box.

Live Example

// Input: Zod schema
import { z } from 'zod';

const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  age: z.number().int().min(0).max(120),
  role: z.enum(['admin', 'user', 'guest']),
  bio: z.string().nullable(),
  tags: z.array(z.string()).max(10),
});

// Output: JSON Schema (draft 2020-12)
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "id":    { "type": "string", "format": "uuid" },
    "email": { "type": "string", "format": "email" },
    "age":   { "type": "integer", "minimum": 0, "maximum": 120 },
    "role":  { "type": "string", "enum": ["admin", "user", "guest"] },
    "bio":   { "type": ["string", "null"] },
    "tags":  { "type": "array", "items": { "type": "string" }, "maxItems": 10 }
  },
  "required": ["id", "email", "age", "role", "bio", "tags"]
}

z.toJSONSchema(), the npm Library, and This Tool

There are three common routes, and it's worth knowing which fits:

  • Zod v4 built-in: z.toJSONSchema(schema) ships in the core library — the right call if you're already on v4 and want it in your own runtime.
  • zod-to-json-schema (npm): the mature package for Zod v3 projects, with options for target draft and $ref strategy.
  • This converter: in-browser, nothing uploaded, and it sits next to TypeScript / OpenAPI / mock output so you can move a schema between representations in one place — useful for a quick draft or when you don't want to wire a build step.

For a permanent, in-code pipeline, prefer the built-in or the npm library. For exploration, drafting, and privacy-sensitive payloads, do it here.

How Constraints Map

Unlike a TypeScript type, JSON Schema can express runtime constraints, so far more of the Zod schema survives:

  • z.string().min(3).max(20)"minLength": 3, "maxLength": 20
  • z.string().email() / .uuid() / .url()"format": "email" | "uuid" | "uri"
  • z.string().regex(re)"pattern": "..."
  • z.number().int()"type": "integer"; .min(0)"minimum": 0
  • z.array(T).min(1).max(10)"minItems": 1, "maxItems": 10
  • z.enum([...])"enum": [...]; z.literal(x)"const": x
  • .optional() → omitted from required; .nullable()"type": ["T", "null"]
  • .describe('...')"description": "..."; .default(v)"default": v

Shared Types Become $defs + $ref

Reused sub-schemas are emitted once under $defs and referenced, keeping the document DRY and matching how OpenAPI's components/schemas works:

const AddressSchema = z.object({ city: z.string(), zip: z.string() });
const OrderSchema = z.object({
  billing: AddressSchema,
  shipping: AddressSchema,
});

// →
{
  "type": "object",
  "properties": {
    "billing":  { "$ref": "#/$defs/Address" },
    "shipping": { "$ref": "#/$defs/Address" }
  },
  "required": ["billing", "shipping"],
  "$defs": {
    "Address": {
      "type": "object",
      "properties": { "city": { "type": "string" }, "zip": { "type": "string" } },
      "required": ["city", "zip"]
    }
  }
}

Unions, Discriminated Unions, and null

z.union([z.string(), z.number()])
// → { "anyOf": [ { "type": "string" }, { "type": "number" } ] }

z.discriminatedUnion('type', [A, B])
// → { "oneOf": [ ...A, ...B ] }   (the discriminator stays a const on each branch)

z.string().nullable()
// → { "type": ["string", "null"] }

Use Case: LLM Structured Outputs

OpenAI and Anthropic tool/function calling expect a JSON Schema for the arguments. If you already validate with Zod, convert it instead of hand-writing the schema twice:

const ExtractSchema = z.object({
  sentiment: z.enum(['positive', 'neutral', 'negative']),
  score: z.number().min(0).max(1),
  topics: z.array(z.string()),
});

// Feed the converted JSON Schema as the tool's parameters,
// then validate the model's response back through ExtractSchema.
// One Zod schema drives both the request contract and the runtime check.

Note: providers in "strict" mode reject some keywords (e.g. they may require additionalProperties: false and disallow certain formats). Treat the output as a draft and trim it to the provider's accepted subset.

Draft Versions

JSON Schema has incompatible dialects. Draft-07 is the most widely supported by older tooling. 2020-12 aligns with OpenAPI 3.1 and is the modern default. The biggest practical differences: $defs (2020-12) vs definitions (draft-07), and how nullable and tuples are expressed. Pick the draft your consumer expects — emitting 2020-12 into a draft-07 validator is a common source of silent mismatches.

What Doesn't Translate

  • .transform() / .pipe(): JSON Schema validates, it doesn't transform. Transforms are dropped — keep them in Zod.
  • Custom .refine() logic: arbitrary predicates have no declarative equivalent; they can't be represented and are omitted.
  • Custom error messages: Zod's per-rule messages don't map to standard JSON Schema and are lost.

Because of these, a converted schema validates the shape and declarative constraints, not your transform/refine logic. Zod remains the source of truth for anything procedural.

FAQ

Q: draft-07 or 2020-12?
A: Match the consumer. Use 2020-12 for OpenAPI 3.1 and modern validators; draft-07 for older tooling that doesn't understand $defs or 2020-12 keywords.

Q: Why is a .refine() rule missing from the output?
A: Custom refinements are arbitrary code with no JSON Schema equivalent, so they're dropped. Declarative rules (min, max, regex, email) do convert. Keep procedural checks in Zod.

Q: Can I use this for OpenAPI?
A: Yes — OpenAPI 3.1's schema objects are JSON Schema 2020-12. Drop the converted schema into components/schemas. (TypeMorph also has a direct Zod → OpenAPI path.)

Q: How are optional vs nullable handled?
A: .optional() removes the key from required; .nullable() adds "null" to the type. .nullish() does both.

Q: Is my schema uploaded anywhere?
A: No. The conversion runs entirely in your browser — nothing is sent to a server.

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.