Capítulo 45 de 61

Chapter 45: JSON Schema — Conversion Functions

Core Idea

z.toJSONSchema() converts a Zod schema into a standard JSON Schema document (useful for OpenAPI specs and AI structured-output definitions); the experimental z.fromJSONSchema() goes the other direction.

Key Concepts

  • z.toJSONSchema(schema, params?): converts to JSON Schema, defaulting to Draft 2020-12. Every check/type is mapped to its closest JSON Schema equivalent.
  • z.fromJSONSchema(jsonSchema): experimental, converts a JSON Schema object into a Zod schema — not considered stable API yet.
  • Unrepresentable types: bigint, int64, symbol, undefined, void, date, map, set, transform, nan, and custom have no JSON Schema equivalent. Default behavior is to throw; unrepresentable: "any" converts them to {} (JSON Schema's "anything"); a function lets you decide per-schema, returning a replacement JSON Schema, "any", or "throw" — it receives zodSchema, path, and message for building a precise error or substitution.
  • io: "input" | "output" (default "output"): schemas whose input/output diverge (pipes, .default(), coerced primitives) — io: "input" extracts the pre-transform shape instead of the post-transform one.
  • target: JSON Schema dialect — "draft-2020-12" (default), "draft-07", "draft-04", or "openapi-3.0".
  • metadata: .meta({...}) (a shortcut for registering in z.globalRegistry) copies arbitrary fields straight into the output JSON Schema, and metadata overrides whatever keyword Zod would otherwise generate — including things like type itself.

Code Examples

const schema = z.object({ name: z.string(), age: z.number() });
z.toJSONSchema(schema);
// => { type: "object", properties: { name: {type:"string"}, age: {type:"number"} }, required: ["name","age"], additionalProperties: false }

// unrepresentable types
z.toJSONSchema(z.bigint());                          // throws by default
z.toJSONSchema(z.bigint(), { unrepresentable: "any" }); // => {}

// io: input vs output side of a transforming pipe
const piped = z.string().transform(val => val.length).pipe(z.number());
z.toJSONSchema(piped);                    // => { type: "number" } (output)
z.toJSONSchema(piped, { io: "input" });   // => { type: "string" } (input)

// metadata copies through, and overrides generated keywords
const emailSchema = z.string().meta({ title: "Email address", description: "Your email address" });
z.toJSONSchema(emailSchema);
// => { type: "string", title: "Email address", description: "Your email address" }

z.toJSONSchema(z.string().meta({ type: "number" }));
// => { type: "number" }  — metadata wins over the string type Zod would normally emit
  • What it demonstrates: the default conversion, handling of types with no JSON Schema equivalent, and metadata both flowing through and overriding generated output.

Anti-patterns

  • Assuming every Zod type converts cleanly to JSON Schema: bigint, date, Map/Set, and custom schemas need explicit handling (unrepresentable option) — don't assume z.toJSONSchema() "just works" on an arbitrary schema without checking for these.

Key Takeaways

  1. unrepresentable is the escape hatch for the handful of Zod types JSON Schema fundamentally can't express — decide up front whether to throw, degrade to {}, or supply a manual replacement.
  2. .meta() fields aren't just documentation — they can override the generated JSON Schema output entirely, which is powerful but means a stray .meta({ type: ... }) can silently change the emitted schema.
  3. Use io: "input" whenever the JSON Schema needs to describe what callers send, not what the schema ultimately produces after transforms.

Connects To

  • Metadata & Registries: .meta()/z.globalRegistry, the mechanism this chapter's metadata handling builds on.
  • JSON Schema — Conversion Details & Registries: cycle handling, schema reuse, and the override option.