This document covers Zod's native JSON Schema conversion system, which transforms Zod schemas into JSON Schema representations. The system supports multiple JSON Schema drafts (2020-12, draft-07, draft-04) and OpenAPI 3.0 Schema Objects. For information about converting JSON Schema to Zod schemas, see Extensions and Integrations. For metadata and registry concepts, see Metadata and Registry System.
The JSON Schema conversion system provides a multi-stage pipeline that transforms Zod schemas into JSON Schema objects. The system handles complex scenarios including circular references, schema reuse, multiple target formats, and bidirectional transforms. The main entry point is the toJSONSchema() function exported from both the Classic and Mini API variants.
Sources: packages/zod/src/v4/core/to-json-schema.ts1-614 packages/zod/src/v4/core/json-schema-processors.ts609-667 packages/docs/content/json-schema.mdx1-593
Sources: packages/zod/src/v4/core/to-json-schema.ts118-137 packages/zod/src/v4/core/to-json-schema.ts139-215 packages/zod/src/v4/core/to-json-schema.ts217-357 packages/zod/src/v4/core/to-json-schema.ts359-516
The initializeContext() function creates a ToJSONSchemaContext object that maintains state throughout the conversion process. It normalizes parameters, sets up the processor registry, and initializes the seen map for cycle detection.
| Parameter | Type | Default | Description |
|---|---|---|---|
processors | Record<string, Processor> | {} | Type-specific conversion handlers |
metadataRegistry | $ZodRegistry | globalRegistry | Source for schema metadata |
target | string | "draft-2020-12" | Target JSON Schema version |
unrepresentable | "throw" | "any" | "throw" | How to handle unrepresentable types |
io | "input" | "output" | "output" | Which schema type to extract |
cycles | "ref" | "throw" | "ref" | How to handle circular references |
reused | "ref" | "inline" | "inline" | How to handle reused schemas |
Sources: packages/zod/src/v4/core/to-json-schema.ts118-137 packages/zod/src/v4/core/to-json-schema.ts84-107
The process() function performs depth-first traversal of the schema tree, invoking type-specific processors and maintaining a seen map to detect cycles and track reuse.
Sources: packages/zod/src/v4/core/to-json-schema.ts139-215
The extractDefs() function determines which schemas should be extracted to the $defs section based on three criteria:
id in the metadata registryreused: "ref")The makeURI() helper generates appropriate $ref URIs based on the target format:
| Target | Defs Segment | URI Format |
|---|---|---|
draft-2020-12 | $defs | #/$defs/{id} |
draft-07, draft-04 | definitions | #/definitions/{id} |
| External registry | varies | Custom URI via uri() parameter |
Sources: packages/zod/src/v4/core/to-json-schema.ts217-357 packages/zod/src/v4/core/to-json-schema.ts242-275
The finalize() function completes the conversion by:
$schema property for the target format$defs or definitions object~standard property for Standard Schema compatibilitySources: packages/zod/src/v4/core/to-json-schema.ts359-516 packages/zod/src/v4/core/to-json-schema.ts367-447
Each Zod schema type has a dedicated processor function that implements type-specific conversion logic. Processors are registered in the allProcessors map and invoked during the process() stage.
Processors directly modify the json object rather than returning a new value. This enables efficient in-place updates during traversal.
Sources: packages/zod/src/v4/core/to-json-schema.ts7-12
| Schema Type | Processor Function | JSON Schema Output | Notes |
|---|---|---|---|
string | stringProcessor | {type: "string"} | Handles format, pattern, length constraints |
number | numberProcessor | {type: "number"} or {type: "integer"} | Handles min/max, exclusiveMin/Max, multipleOf |
boolean | booleanProcessor | {type: "boolean"} | Direct mapping |
null | nullProcessor | {type: "null"} | OpenAPI 3.0 uses nullable: true |
any | anyProcessor | {} | Empty schema accepts anything |
unknown | unknownProcessor | {} | Empty schema accepts anything |
never | neverProcessor | {not: {}} | Rejects all values |
Sources: packages/zod/src/v4/core/json-schema-processors.ts27-152
The stringProcessor converts Zod string format checks to JSON Schema format and pattern properties:
Format mappings in packages/zod/src/v4/core/json-schema-processors.ts17-23:
| Zod Format | JSON Schema Format |
|---|---|
email | "email" |
uuid | "uuid" |
guid | "uuid" |
url | "uri" |
datetime | "date-time" |
Sources: packages/zod/src/v4/core/json-schema-processors.ts27-60
| Schema Type | Processor | Conversion Strategy |
|---|---|---|
object | objectProcessor | Maps shape to properties, handles required, additionalProperties |
array | arrayProcessor | Converts element to items, handles minItems/maxItems |
tuple | tupleProcessor | Uses prefixItems (2020-12) or items array (older drafts) |
union | unionProcessor | Generates anyOf (inclusive) or oneOf (exclusive) |
intersection | intersectionProcessor | Generates allOf with flattened nested intersections |
record | recordProcessor | Uses propertyNames/additionalProperties or patternProperties |
Sources: packages/zod/src/v4/core/json-schema-processors.ts281-477
The objectProcessor handles several object-specific concerns:
optin/optout markersfalse for output mode (default Zod behavior strips unknowns)additionalProperties when presentSources: packages/zod/src/v4/core/json-schema-processors.ts292-336
The recordProcessor handles two distinct patterns:
propertyNames + additionalPropertiespatternProperties for proper intersection semanticsSources: packages/zod/src/v4/core/json-schema-processors.ts430-477
Wrapper schemas (optional, nullable, default, etc.) typically process their inner type and set a ref in the Seen entry to enable property inheritance during finalization:
| Wrapper Type | Processor | Special Handling |
|---|---|---|
optional | optionalProcessor | Sets seen.ref, no JSON Schema changes |
nullable | nullableProcessor | OpenAPI 3.0: nullable: true, others: anyOf with null |
default | defaultProcessor | Adds default property to JSON Schema |
prefault | prefaultProcessor | Adds _prefault (removed in output mode) |
readonly | readonlyProcessor | Adds readOnly: true |
pipe | pipeProcessor | Uses in or out based on ctx.io |
lazy | lazyProcessor | Resolves via schema._zod.innerType |
Sources: packages/zod/src/v4/core/json-schema-processors.ts479-563
The target parameter determines the JSON Schema version and affects several conversion aspects:
| Target | $schema Value | $defs Segment | Exclusive Min/Max | Nullable Handling |
|---|---|---|---|---|
draft-2020-12 | https://json-schema.org/draft/2020-12/schema | $defs | Numeric value | anyOf with {type: "null"} |
draft-07 | http://json-schema.org/draft-07/schema# | definitions | Numeric value | anyOf with {type: "null"} |
draft-04 | http://json-schema.org/draft-04/schema# | definitions | Boolean flag + minimum | anyOf with {type: "null"} |
openapi-3.0 | (none) | definitions | Boolean flag + minimum | nullable: true property |
Sources: packages/zod/src/v4/core/to-json-schema.ts14-48 packages/zod/src/v4/core/to-json-schema.ts454-464
The io parameter determines whether to convert the input or output type of transforming schemas:
When io === "input":
examples and default are stripped (apply to output type)_prefault is moved to defaultadditionalProperties: false is not set on objects (input doesn't strip)Sources: packages/zod/src/v4/core/to-json-schema.ts201-209 packages/docs/content/json-schema.mdx131-144
Several Zod types have no JSON Schema equivalent:
| Zod Type | Reason | Error When unrepresentable: "throw" |
|---|---|---|
bigint | JSON has no bigint type | "BigInt cannot be represented in JSON Schema" |
symbol | Symbols don't serialize to JSON | "Symbols cannot be represented in JSON Schema" |
date | Dates serialize as strings | "Date cannot be represented in JSON Schema" |
map | Maps don't exist in JSON | "Map cannot be represented in JSON Schema" |
set | Sets don't exist in JSON | "Set cannot be represented in JSON Schema" |
transform | Transforms are runtime behavior | "Transforms cannot be represented in JSON Schema" |
custom | Custom validators are opaque | "Custom types cannot be represented in JSON Schema" |
With unrepresentable: "any", these types produce {} (equivalent to unknown in JSON Schema).
Sources: packages/zod/src/v4/core/json-schema-processors.ts107-277 packages/docs/content/json-schema.mdx205-235
| Parameter | Value | Behavior |
|---|---|---|
cycles | "ref" (default) | Extract cyclic schemas to $defs, use $ref to break cycle |
cycles | "throw" | Throw error when cycle detected |
reused | "inline" (default) | Inline schemas used multiple times |
reused | "ref" | Extract reused schemas to $defs |
Cycle detection occurs in process() by checking if a schema exists in params.schemaPath packages/zod/src/v4/core/to-json-schema.ts153-156
Sources: packages/zod/src/v4/core/to-json-schema.ts98-99 packages/zod/src/v4/core/to-json-schema.ts302-313 packages/docs/content/json-schema.mdx236-262
When passed a $ZodRegistry instead of a single schema, toJSONSchema() converts all schemas in the registry and returns a {schemas: {...}} object. This enables multi-schema conversion with cross-references.
Example structure:
All schemas in the registry must have an id property in their metadata. Cross-references between schemas use the uri() function to generate absolute URIs packages/zod/src/v4/core/to-json-schema.ts250-263
Sources: packages/zod/src/v4/core/json-schema-processors.ts623-659 packages/docs/content/json-schema.mdx497-592
The override parameter accepts a callback function that can modify the generated JSON Schema for any schema in the tree:
The override function is called after processor execution but before ref flattening packages/zod/src/v4/core/to-json-schema.ts442-446 This allows customization of unrepresentable types when combined with unrepresentable: "any".
Sources: packages/zod/src/v4/core/to-json-schema.ts29-34 packages/docs/content/json-schema.mdx304-336
The conversion result includes a non-enumerable ~standard property that provides input() and output() methods conforming to the Standard Schema specification:
These methods are created via createStandardJSONSchemaMethod() packages/zod/src/v4/core/to-json-schema.ts605-613 and enable dynamic re-conversion with different parameters without re-running the full pipeline.
Sources: packages/zod/src/v4/core/to-json-schema.ts500-511 packages/zod/src/v4/core/to-json-schema.ts605-613 packages/zod/src/v4/mini/tests/standard-schema.test.ts19-50
The ctx.seen map tracks all schemas encountered during traversal. Each entry contains:
| Property | Type | Purpose |
|---|---|---|
schema | JSONSchema.BaseSchema | Mutable JSON Schema object being built |
def | JSONSchema.BaseSchema? | Cached copy before $ref replacement |
defId | string? | Identifier for use in $defs |
count | number | Number of times schema was encountered |
cycle | (string | number)[]? | Path where cycle was detected |
ref | $ZodType? | Parent schema to inherit properties from |
path | (string | number)[]? | JSON Schema property path |
Sources: packages/zod/src/v4/core/to-json-schema.ts67-82
The flattenRef() function in finalize() handles property inheritance from parent schemas:
seen.ref is null (already processed) or undefined (no parent)defThis ensures wrapper schemas like z.string().optional().nullable() correctly inherit {type: "string"} from the base string schema.
Sources: packages/zod/src/v4/core/to-json-schema.ts367-439
Tuple conversion varies significantly by target format:
| Target | items | Rest Items | Length Constraints |
|---|---|---|---|
draft-2020-12 | prefixItems array | items (single schema) | Via minItems/maxItems |
draft-07, draft-04 | Array of schemas | additionalItems | Via minItems/maxItems |
openapi-3.0 | {anyOf: [...]} with all options | Appended to anyOf | minItems = length, maxItems only if no rest |
The OpenAPI 3.0 approach flattens the tuple structure since it doesn't support prefixItems packages/zod/src/v4/core/json-schema-processors.ts402-413
Sources: packages/zod/src/v4/core/json-schema-processors.ts375-428
Metadata from the registry is applied to the JSON Schema object after processor execution packages/zod/src/v4/core/to-json-schema.ts198-199 All properties from the metadata object are copied directly via Object.assign(), allowing arbitrary properties to pass through to the final JSON Schema.
Refresh this wiki