7. Metadata Registries & JSON Schema
Zod v4 introduces a first-class Metadata Registry system (z.registry() & z.globalRegistry), native bidirectional JSON Schema conversion (z.toJSONSchema() & z.fromJSONSchema()), and built-in Standard Schema v1 (~standard) support—turning your Zod schemas into a single source of truth for runtime validation, OpenAPI specs, LLM tool definitions, and full-stack form libraries.
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.lines import line
from drawlib.shapes import rectangle
from drawlib.smartarts import Panel
from drawlib.styles import Colors, Styles
from drawlib.text import text
setup(width=160, height=76)
text(
(80, 71.5),
"Metadata Registries & Bidirectional JSON Schema Conversion",
style=Styles.PrimaryBold.patch(text_size=13.5),
)
# Left Panel: Metadata Registries
Panel(
"1. WeakMap Metadata Registries",
style=Styles.Neutral.patch(shape_fill_color=Colors.White),
header_style=Styles.PrimaryNeutral,
text_style=Styles.PrimaryBold.patch(text_size=11.0),
header_align="center",
).draw((6, 6), width=48, height=60)
rectangle((30, 48), width=40, height=13, style=Styles.PrimaryNeutral.patch(shape_r=1.5))
text((30, 51.2), "z.globalRegistry", style=Styles.PrimaryBold.patch(text_size=11.0))
text((30, 47.4), ".meta({ id, title, description })", style=Styles.Neutral.patch(text_size=10.0))
text((30, 44.0), ".describe('...')", style=Styles.Neutral.patch(text_size=10.0))
rectangle((30, 29), width=40, height=13, style=Styles.SecondaryNeutral.patch(shape_r=1.5))
text((30, 32.2), "z.registry<MyMeta>()", style=Styles.SecondaryBold.patch(text_size=11.0))
text((30, 28.4), "Custom Typed Metadata", style=Styles.Neutral.patch(text_size=10.0))
text((30, 25.0), "Supports z.$output type token", style=Styles.Neutral.patch(text_size=10.0))
rectangle((30, 13.5), width=40, height=8.5, style=Styles.Neutral.patch(shape_r=1.5))
text((30, 15.2), "Shared via globalThis", style=Styles.PrimaryBold.patch(text_size=10.0))
text((30, 11.8), "Deduplicated across ESM & CJS", style=Styles.Neutral.patch(text_size=10.0))
# Center hub: Zod v4 Schema AST
rectangle((80, 36), width=32, height=28, style=Styles.PrimaryFlat.patch(shape_r=2.0))
phosphor.brackets_curly((80, 44.5), width=6.0, style=Styles.White)
text((80, 37.5), "Zod v4 Schema", style=Styles.WhiteBold.patch(text_size=12.0))
text((80, 33.2), "_zod.def + checks[]", style=Styles.White.patch(text_size=10.0))
text((80, 29.2), "Parent Inheritance", style=Styles.White.patch(text_size=10.0))
text((80, 25.2), "Recursive $defs", style=Styles.White.patch(text_size=10.0))
line((54, 48), (64, 41), arrow_head="<->", style=Styles.PrimaryBold)
line((54, 29), (64, 31), arrow_head="<->", style=Styles.SecondaryBold)
# Right Panel: Bidirectional JSON Schema
Panel(
"2. JSON Schema Interop",
style=Styles.Neutral.patch(shape_fill_color=Colors.White),
header_style=Styles.PrimaryNeutral,
text_style=Styles.PrimaryBold.patch(text_size=11.0),
header_align="center",
).draw((106, 6), width=48, height=60)
rectangle((130, 47), width=40, height=15, style=Styles.AccentFlat.patch(shape_r=1.5))
text((130, 51.0), "z.toJSONSchema()", style=Styles.WhiteBold.patch(text_size=11.0))
text((130, 47.0), "draft-2020-12 / draft-07", style=Styles.White.patch(text_size=10.0))
text((130, 43.2), "openapi-3.0 | io: in/out", style=Styles.White.patch(text_size=10.0))
rectangle((130, 28), width=40, height=13, style=Styles.PrimaryNeutral.patch(shape_r=1.5))
text((130, 31.2), "z.fromJSONSchema()", style=Styles.PrimaryBold.patch(text_size=11.0))
text((130, 27.4), "JSON Schema -> ZodType", style=Styles.Neutral.patch(text_size=10.0))
text((130, 24.0), "Resolves local #/$defs pointers", style=Styles.Neutral.patch(text_size=10.0))
rectangle((130, 13.5), width=40, height=8.5, style=Styles.Neutral.patch(shape_r=1.5))
text((130, 15.2), "OpenAPI / LLM Tool Specs", style=Styles.PrimaryBold.patch(text_size=10.0))
text((130, 11.8), "Structured Outputs & Codegen", style=Styles.Neutral.patch(text_size=10.0))
line((96, 42), (110, 47), arrow_head="->", style=Styles.PrimaryBold)
line((110, 28), (96, 31), arrow_head="->", style=Styles.PrimaryBold)
save()
1. Metadata Registries (z.globalRegistry & .meta())
Instead of bloating every schema instance with arbitrary documentation properties, Zod v4 stores schema metadata in WeakMap-backed registries ($ZodRegistry). This design allows garbage collection of ephemeral schemas while keeping metadata decoupled and strongly typed.
Using .meta() and .describe() with z.globalRegistry
Every classic Zod schema provides .meta(metadata) and .describe(text) convenience methods that register metadata in z.globalRegistry (which is attached to globalThis.__zod_globalRegistry so ESM and CJS bundles share a single deduplicated instance):
import * as z from "zod";
const EmailSchema = z.email().meta({
id: "EmailAddress",
title: "Email Address",
description: "Primary contact email for the account",
examples: ["alice@example.com"],
});
// Read metadata directly from the schema or from z.globalRegistry
console.log(EmailSchema.meta());
// { id: "EmailAddress", title: "Email Address", description: "...", examples: [...] }
// .describe() is a shorthand for .meta({ description: "..." })
const UsernameSchema = z.string().min(3).describe("Public handle");
console.log(z.globalRegistry.get(UsernameSchema)?.description);
// "Public handle"
Note on Metadata Inheritance: When you derive a new schema via
.optional(),.nullable(), or.refine(),registry.get(derivedSchema)automatically inherits metadata from its parent schema—except forid, which remains unique to the original schema to prevent duplicate$defsidentifiers during JSON Schema export.
2. Custom Typed Registries (z.registry<Meta>())
When building form builders, database ORMs, or CLI generators, you can create isolated, strongly-typed registries with z.registry<Meta, Schema>().
Using the special z.$output and z.$input type tokens inside your metadata interface tells Zod to substitute the specific schema's inferred output or input type at registration time:
import * as z from "zod";
// Define a metadata type parameterized by each schema's inferred output type
interface FormFieldMeta {
label: string;
placeholder?: string;
defaultValue?: z.$output;
examples?: z.$output[];
validateOnBlur?: boolean;
}
const formRegistry = z.registry<FormFieldMeta>();
const AgeField = z.number().int().min(0);
const NameField = z.string().min(1);
// Type-checked! `defaultValue` and `examples` must be `number` for AgeField
formRegistry.add(AgeField, {
label: "Age",
defaultValue: 25,
examples: [18, 25, 40],
});
// Type-checked! `defaultValue` must be `string` for NameField
formRegistry.add(NameField, {
label: "Full Name",
placeholder: "Jane Doe",
defaultValue: "Jane Doe",
});
// Retrieve strongly-typed metadata
const ageMeta = formRegistry.get(AgeField);
// ageMeta?.defaultValue is inferred as `number | undefined`
You can also register metadata inline from a schema chain using schema.register(myRegistry, meta):
const RoleField = z
.enum(["admin", "editor", "viewer"])
.register(formRegistry, {
label: "Workspace Role",
defaultValue: "viewer",
});
3. Converting Zod to JSON Schema (z.toJSONSchema())
Zod v4 replaces external zod-to-json-schema libraries with a built-in, spec-compliant z.toJSONSchema(schemaOrRegistry, params?) engine.
import * as z from "zod";
const UserSchema = z
.object({
id: z.uuid(),
name: z.string().min(1),
role: z.enum(["admin", "user"]).default("user"),
tags: z.array(z.string()).optional(),
})
.meta({
id: "User",
title: "User Account",
description: "Registered platform user",
});
const jsonSchema = z.toJSONSchema(UserSchema);
/*
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"id": "User",
"title": "User Account",
"description": "Registered platform user",
"type": "object",
"properties": {
"id": { "type": "string", "format": "uuid", "pattern": "..." },
"name": { "type": "string", "minLength": 1 },
"role": { "type": "string", "enum": ["admin", "user"], "default": "user" },
"tags": { "type": "array", "items": { "type": "string" } }
},
"required": ["id", "name", "role"],
"additionalProperties": false
}
*/
Configuration Options (ToJSONSchemaParams)
| Option | Values | Default | Behavior |
|---|---|---|---|
target |
"draft-2020-12" | "draft-07" | "draft-04" | "openapi-3.0" |
"draft-2020-12" |
Controls $schema URI, nullable: true (OpenAPI 3.0), prefixItems vs items, and $defs vs definitions. |
io |
"output" | "input" |
"output" |
"output" emits the post-transform/post-default schema (where .default() fields are required); "input" emits the wire input schema (where .default() fields are optional). |
unrepresentable |
"throw" | "any" | (ctx) => ... |
"throw" |
Controls how non-JSON types (z.bigint(), z.date(), z.map(), z.symbol(), z.transform()) are handled. Pass "any" to emit {} instead of throwing. |
cycles |
"ref" | "throw" |
"ref" |
Automatically resolves recursive z.lazy() / getter schemas into JSON Pointer $ref entries under $defs. |
reused |
"inline" | "ref" |
"inline" |
When set to "ref", any subschema referenced multiple times is automatically extracted into $defs. Schemas with .meta({ id }) are always extracted when referenced. |
metadata |
$ZodRegistry |
z.globalRegistry |
Custom registry used to populate metadata properties and $defs IDs. |
override |
(ctx) => void |
— | Post-processor hook receiving { zodSchema, jsonSchema, path } for custom vendor extensions (x-*). |
Exporting an Entire Registry & Handling Recursive Schemas
When you assign an id in .meta({ id: "Category" }), z.toJSONSchema() uses that id as the key in $defs for recursive references—or you can pass an entire $ZodRegistry directly to z.toJSONSchema(registry) to generate a multi-schema bundle:
import * as z from "zod";
const Category = z
.object({
name: z.string(),
get subcategories() {
return z.array(Category);
},
})
.meta({ id: "Category" });
const treeJsonSchema = z.toJSONSchema(Category, {
target: "draft-2020-12",
cycles: "ref",
});
// Emits `$defs.Category` with `"subcategories": { "type": "array", "items": { "$ref": "#/$defs/Category" } }`
4. Converting JSON Schema to Zod (z.fromJSONSchema())
Zod v4 also provides z.fromJSONSchema(jsonSchema, params?) in zod (classic) to hydrate external JSON Schema documents (Draft 2020-12, Draft 7, Draft 4, or OpenAPI 3.0) into runtime ZodType validators—complete with local #/$defs/... and #/definitions/... reference resolution:
import * as z from "zod";
const rawJsonSchema = {
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: {
productId: { type: "string", format: "uuid" },
price: { type: "number", minimum: 0, exclusiveMinimum: 0 },
tags: { type: "array", items: { type: "string" }, minItems: 1 },
},
required: ["productId", "price"],
additionalProperties: false,
};
// Reconstruct a runtime Zod schema from JSON Schema
const ProductSchema = z.fromJSONSchema(rawJsonSchema);
const parsed = ProductSchema.safeParse({
productId: "550e8400-e29b-41d4-a716-446655440000",
price: 29.99,
tags: ["typescript"],
});
console.log(parsed.success); // true
5. Standard Schema v1 & Standard JSON Schema v1 (~standard)
Every Zod v4 schema (both zod and zod/mini) implements the cross-library Standard Schema v1 and Standard JSON Schema v1 specifications via the schema["~standard"] property.
Direct Usage of schema["~standard"]
Framework authors and generic libraries can validate inputs and generate JSON Schemas without importing Zod directly—any Standard Schema v1 object exposes .validate() and .jsonSchema:
import * as z from "zod";
import type { StandardSchemaWithJSON } from "zod/v4/core";
const OrderSchema = z.object({
orderId: z.uuid(),
quantity: z.number().int().min(1),
});
// Generic function accepting any Standard Schema v1 + Standard JSON Schema v1
async function executeTool<S extends StandardSchemaWithJSON>(
schema: S,
rawPayload: unknown
) {
const std = schema["~standard"];
// 1. Generate OpenAPI 3.0 or Draft 2020-12 JSON Schema for LLM tool definitions
const inputJsonSchema = std.jsonSchema.input({ target: "draft-2020-12" });
// 2. Validate incoming payload via Standard Schema v1
const result = await std.validate(rawPayload);
if (result.issues) {
throw new Error(z.prettifyError(result));
}
return {
toolSchema: inputJsonSchema,
verifiedData: result.value,
};
}
Next Steps
- Proceed to 8. Tree-Shaking with zod/mini & JIT Compilation to learn how
zod/miniachieves ~2 KB frontend bundles and howzod/compilegenerates ultra-fast JIT validators. - Return to 6. Error Formatting & 49 i18n Locales for error formatting details.