Enhanced fork of json-schema-to-zod - Converts JSON Schema into Zod schemas with fluent builder pattern
npm install x-to-zod

> Note: This is an enhanced fork of json-schema-to-zod by Stefan Terdell. All credit for the original implementation goes to the original author and contributors.
A runtime package and CLI tool to convert JSON Schema draft-2020-12 objects or files into Zod schemas in the form of JavaScript code.
TypeScript Type Definitions: This package uses json-schema-typed for comprehensive JSON Schema draft-2020-12 type definitions, providing excellent IntelliSense and type safety. Earlier JSON Schema drafts (such as draft-04/06/07) may work when they use features that are compatible with draft-2020-12, but only draft-2020-12 is explicitly supported.
_Looking for the exact opposite? Check out zod-to-json-schema_
This fork includes several architectural improvements and new features:
build.number(), build.string(), build.object(), etc..min() calls keep the strictest value)BaseBuilder with shared modifiers.optional(), .nullable(), .default(), .describe(), .brand(), .readonly(), .catch(), and morevoid, undefined, date, bigint, symbol, nanurl, httpUrl, hostname, emoji, base64url, hex, jwt, nanoid, cuid, cuid2, ulid, ipv4, ipv6, mac, cidrv4, cidrv6, hash, isoDate, isoTime, isoDatetime, isoDuration, uuidv4, uuidv6, uuidv7set, mappromise, lazy, function, codec, preprocess, pipe, json, file, nativeEnum, templateLiteral, xor, keyofz.xor() instead of manual superRefinemoduleResolution: "bundler" for compatibility with modern bundlerszodVersion option'v3' for backward compatibilityz.strictObject(), z.looseObject(), .extend() instead of .merge()``console`
npm i -g x-to-zod
This package supports importing version-specific builder APIs. This ensures you only use features compatible with your target Zod version and provides TypeScript type safety.
#### Using Zod v4 (default)
`typescript
import { build } from 'x-to-zod/v4';
// All v4 features available
const promiseSchema = build.promise(build.string());
const lazySchema = build.lazy('() => mySchema');
const jsonSchema = build.json();
`
#### Using Zod v3 (backward compatibility)
`typescript
import { build } from 'x-to-zod/v3';
// Only v3-compatible features available
const stringSchema = build.string();
const objectSchema = build.object({ name: build.string() });
// TypeScript error - v4-only features not available:
// const promiseSchema = build.promise(build.string()); // ❌
`
Benefits:
- Type Safety: TypeScript prevents using v4-only features when importing from v3
- Explicit Intent: Makes your Zod version dependency clear in code
- Future-Proof: Easier migration when Zod releases new versions
Default Import: The main package export includes all features (v4-compatible):
`typescript`
import { build } from 'x-to-zod';
// Same as 'x-to-zod/v4'
#### Simplest example
`console`
x-to-zod -i mySchema.json -o mySchema.ts
#### Example with $refs resolved and output formatted
`console
npm i -g x-to-zod json-refs prettier
`console`
json-refs resolve mySchema.json | x-to-zod | prettier --parser typescript > mySchema.ts
#### Options
| Flag | Shorthand | Function |
| -------------- | --------- | ---------------------------------------------------------------------------------------------- |
| --input | -i | JSON or a source file path. Required if no data is piped. |--name
| | -n | The name of the schema in the output |--depth
| | -d | Maximum depth of recursion in schema before falling back to z.any(). Defaults to 0. |--module
| | -m | Module syntax; esm, cjs or none. Defaults to esm in the CLI and none programmatically. |--type
| | -t | Export a named type along with the schema. Requires name to be set and module to be esm. |--withJsdocs
| | -wj | Generate jsdocs off of the description property. |
#### Simple example
`typescript
import { jsonSchemaToZod } from "x-to-zod";
const myObject = {
type: "object",
hello: {
type: "string",
},
},
};
const module = jsonSchemaToZod(myObject, { module: "esm" });
// type can be either a string or - outside of the CLI - a boolean. If it's true,
// the name of the type will be the name of the schema with a capitalized first letter.
const moduleWithType = jsonSchemaToZod(myObject, {
name: "mySchema",
module: "esm",
type: true,
});
const cjs = jsonSchemaToZod(myObject, { module: "cjs", name: "mySchema" });
const justTheSchema = jsonSchemaToZod(myObject);
`
Transform Zod builders after parsing with post-processors:
`typescript
import { jsonSchemaToZod } from "x-to-zod";
import { is } from "x-to-zod/utils";
const schema = {
type: "object",
properties: {
name: { type: "string" },
tags: { type: "array", items: { type: "string" } }
}
};
// Make all objects strict
const result = jsonSchemaToZod(schema, {
postProcessors: [
{
processor: (builder) => {
if (is.objectBuilder(builder)) {
return builder.strict();
}
return builder;
},
typeFilter: 'object'
}
]
});
// Multiple processors with type filtering
const enhanced = jsonSchemaToZod(schema, {
postProcessors: [
// Make objects strict
{
processor: (builder) => is.objectBuilder(builder) ? builder.strict() : builder,
typeFilter: 'object'
},
// Require non-empty arrays
{
processor: (builder) => is.arrayBuilder(builder) ? builder.min(1) : builder,
typeFilter: 'array'
}
]
});
`
See Post-Processing Guide for more examples and use cases.
NEW in v0.6.0: Support for multi-schema projects with cross-schema references.
Work with multiple JSON Schemas that reference each other, automatically resolving $ref dependencies and generating organized TypeScript/Zod output files. Perfect for:
- OpenAPI/Swagger components with shared schemas
- Domain-Driven Design with separate schema files per domain entity
- Monorepo projects with isolated schema packages
`typescript
import { SchemaProject } from 'x-to-zod';
const project = new SchemaProject.SchemaProject({
outDir: './generated',
moduleFormat: 'both', // Generate both ESM and CJS
zodVersion: 'v4',
generateIndex: true,
});
// Add schemas with cross-references
project.addSchema('user', {
$id: 'user',
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' }
}
});
project.addSchema('post', {
$id: 'post',
type: 'object',
properties: {
id: { type: 'string' },
title: { type: 'string' },
authorId: { $ref: 'user#/properties/id' } // Cross-schema reference
}
});
await project.build();
// Generates: generated/user.ts, generated/post.ts, generated/index.ts
`
`bashBasic usage
x-to-zod --project --schemas "./schemas/*.json" --out ./generated
See Multi-Schema Projects Guide for complete documentation, API reference, and examples.
Builder API
The
build.* factory creates fluent builders that mirror Zod's API. Each builder supports .text() to produce code and shares common modifiers like .optional(), .nullable(), .default(), .describe(), .brand(), .readonly(), .catch(), .refine(), .superRefine(), .meta(), .transform().- Primitives:
-
build.string() → z.string()
- build.number() → z.number()
- build.boolean() → z.boolean()
- build.bigint() → z.bigint()
- build.symbol() → z.symbol()
- build.nan() → z.nan()
- build.null() → z.null()
- build.undefined() → z.undefined()
- build.void() → z.void()- Structured:
-
build.object(props) → z.object({ ... })
- Helpers: .strict(), .loose(), .catchall(schema), .superRefine(fn), .and(schema), .extend(schema|string), .merge(schema|string), .pick(keys), .omit(keys)
- build.array(item) → z.array(item)
- build.tuple(items) → z.tuple(items)
- build.record(key, value) → z.record(key, value)
- build.map(key, value) → z.map(key, value)
- build.set(item) → z.set(item)- Enums and Literals:
-
build.enum(values) → z.enum(values)
- build.literal(value) → z.literal(value)
- build.nativeEnum(enumObj) → z.nativeEnum(enumObj)- Unions and Intersections:
-
build.union(schemas) → z.union(...)
- build.intersection(a, b) → z.intersection(a, b)
- build.discriminatedUnion(tag, options) → z.discriminatedUnion(tag, options)
- build.xor(schemas) → z.xor(schemas) (exactly one must match)- Functions and Lazy:
-
build.function() → z.function()
- .args(...schemas), .returns(schema)
- build.lazy(getter) → z.lazy()- Pipes and Transforms:
-
build.pipe(input) → z.pipe()
- build.preprocess(fn, schema) → z.preprocess()
- build.codec(parseFn, serializeFn) → z.codec()
- build.json() → z.json()- Strings (validators):
-
build.string().url(), .httpUrl(), .hostname(), .emoji(), .base64url(), .hex(), .jwt(), .nanoid(), .cuid(), .cuid2(), .ulid(), .ipv4(), .ipv6(), .mac(), .cidrv4(), .cidrv6(), .hash(algorithm), .isoDate(), .isoTime(), .isoDatetime(), .isoDuration(), .uuidv4(), .uuidv6(), .uuidv7()- Numbers (constraints):
-
build.number().int(), .min(n), .max(n), .positive(), .negative(), .nonnegative(), .nonpositive(), .multipleOf(n)- Templates and Keys:
-
build.templateLiteral(parts) → z.templateLiteral(parts)
- build.keyof(obj) → z.keyof(obj)$3
`ts
// Object with constraints and shared modifiers
build
.object({ id: build.string().uuidv7(), name: build.string().min(1) })
.strict()
.default({ id: '...', name: '' })
.text();// XOR union (exactly one must match)
build.xor([build.string(), build.number()]).text();
// Function schema
build.function().args(build.string(), build.number()).returns(build.boolean()).text();
`#####
module`typescript
import { z } from "zod";export default z.object({ hello: z.string().optional() });
`#####
moduleWithType`typescript
import { z } from "zod";export const mySchema = z.object({ hello: z.string().optional() });
export type MySchema = z.infer;
`#####
cjs`typescript
const { z } = require("zod");module.exports = { mySchema: z.object({ hello: z.string().optional() }) };
`#####
justTheSchema`typescript
z.object({ hello: z.string().optional() });
`#### Example with
$refs resolved and output formatted`typescript
import { z } from "zod";
import { resolveRefs } from "json-refs";
import { format } from "prettier";
import jsonSchemaToZod from "x-to-zod";async function example(jsonSchema: Record): Promise {
const { resolved } = await resolveRefs(jsonSchema);
const code = jsonSchemaToZod(resolved);
const formatted = await format(code, { parser: "typescript" });
return formatted;
}
`Zod Version Support
This library supports generating schemas compatible with both Zod v3 and v4 through the
zodVersion option.$3
`typescript
import { jsonSchemaToZod } from "x-to-zod";// Generate Zod v3 code (default - backward compatible)
const schemaV3 = jsonSchemaToZod(mySchema);
// or explicitly
const schemaV3Explicit = jsonSchemaToZod(mySchema, { zodVersion: 'v3' });
// Generate Zod v4 code (opt-in for new features)
const schemaV4 = jsonSchemaToZod(mySchema, { zodVersion: 'v4' });
`$3
The
zodVersion option affects how certain Zod constructs are generated:#### Object Strict/Loose Modes
v3 mode (default):
`typescript
// additionalProperties: false
z.object({ name: z.string() }).strict()// passthrough behavior (using .loose() method)
z.object({ name: z.string() }).passthrough()
`v4 mode:
`typescript
// additionalProperties: false
z.strictObject({ name: z.string() })// passthrough behavior
z.looseObject({ name: z.string() })
`#### Object Merge
v3 mode (default):
`typescript
baseObject.merge(otherObject)
`v4 mode:
`typescript
baseObject.extend(otherObject)
`#### Error Messages (Future)
When implemented, error messages will use different parameter names:
v3 mode:
{ message: "error text" }
v4 mode: { error: "error text" }$3
The builder API also respects the
zodVersion option:`typescript
import { build } from "x-to-zod/builders";// v4 mode
build.object({ name: build.string() }, { zodVersion: 'v4' }).strict().text()
// => 'z.strictObject({ "name": z.string() })'
// v3 mode (default)
build.object({ name: build.string() }).strict().text()
// => 'z.object({ "name": z.string() }).strict()'
`$3
#### When to use v3 mode (default)
- Existing projects using Zod v3
- Want to avoid any breaking changes
- Gradual migration to Zod v4
#### When to use v4 mode
- New projects starting with Zod v4
- Ready to adopt v4's improved API
- Want cleaner generated code
#### Migration Steps
1. Start with v3 mode (default) - your existing code continues to work
2. Test thoroughly - ensure all generated schemas work as expected
3. Switch to v4 mode - set
zodVersion: 'v4' when ready
4. Update consuming code - adjust for any Zod v4 API changes
5. Enjoy improved syntax - benefit from cleaner, more concise schemas$3
- Default is v3 for backward compatibility
- Both modes are fully tested and production-ready
- No runtime dependencies on specific Zod versions - generates code strings only
- Mix and match - you can generate different schemas with different versions as needed
Advanced Features
$3
The
parserOverride option allows you to customize the code generation for specific schema nodes. This is useful when you need to:
- Use custom Zod schemas for specific patterns
- Replace generated code with hand-crafted validators
- Integrate with custom validation libraries#### Function Signature
`typescript
type ParserOverride = (
schema: JsonSchemaObject,
refs: Context
) => BaseBuilder | string | void;
`Parameters:
-
schema: The current JSON schema node being processed
- refs: Context object containing:
- path: Array tracking the current position in the schema (e.g., ['allOf', 0, 'properties', 'name'])
- seen: Map for circular reference detection
- All other options passed to jsonSchemaToZod()Return Values:
-
string: Replace the generated code with your custom Zod expression (e.g., 'myCustomSchema')
- BaseBuilder: Return a builder instance to customize generation programmatically
- void (or undefined): Use the default parser behavior for this node#### Example: Replace Specific Schema Nodes
`typescript
import { jsonSchemaToZod } from "x-to-zod";const schema = {
allOf: [
{ type: 'string' },
{ type: 'number' },
{ type: 'boolean', description: 'custom-flag' }
]
};
const code = jsonSchemaToZod(schema, {
parserOverride: (schema, refs) => {
// Target the third element in allOf with specific description
if (
refs.path.length === 2 &&
refs.path[0] === 'allOf' &&
refs.path[1] === 2 &&
schema.type === 'boolean' &&
schema.description === 'custom-flag'
) {
// Replace with custom validation
return 'myCustomBooleanValidator';
}
// Use default behavior for all other nodes
}
});
// Output: z.intersection(z.string(), z.intersection(z.number(), myCustomBooleanValidator))
`#### Example: Use Custom Schemas for Format Strings
`typescript
import { jsonSchemaToZod } from "x-to-zod";const schema = {
type: 'object',
properties: {
email: { type: 'string', format: 'email' },
customId: { type: 'string', format: 'x-custom-id' }
}
};
const code = jsonSchemaToZod(schema, {
parserOverride: (schema, refs) => {
// Replace custom format with your own schema
if (schema.type === 'string' && schema.format === 'x-custom-id') {
return 'customIdSchema.refine((v) => /^ID-\\d{6}$/.test(v))';
}
}
});
`#### Example: Path-Based Conditional Logic
`typescript
const code = jsonSchemaToZod(complexSchema, {
parserOverride: (schema, refs) => {
// Target all schemas under 'definitions'
if (refs.path[0] === 'definitions') {
const defName = refs.path[1];
return sharedSchemas.${defName};
} // Target deeply nested properties
if (refs.path.join('.') === 'properties.user.properties.metadata') {
return 'z.record(z.string(), z.unknown())';
}
}
});
`$3
The
preprocessors option allows you to transform JSON schema nodes before parsing begins. This is useful for:
- Normalizing vendor-specific schema extensions
- Applying global transformations to schemas
- Removing or modifying unsupported keywords
- Injecting default values or constraints#### Function Signature
`typescript
type transformer = (
schema: JsonSchemaObject,
refs: Context
) => JsonSchemaObject | undefined;
`Parameters:
-
schema: The current JSON schema node
- refs: Context object with path tracking and optionsReturn Values:
-
JsonSchemaObject: The transformed schema (replaces the original)
- undefined: Keep the schema unchanged#### Execution Order
Preprocessors run before
parserOverride and before any default parsing:1. Preprocessors transform the schema
2. ParserOverride can replace code generation
3. Default parsers generate Zod code
#### Example: Normalize Vendor Extensions
`typescript
import { jsonSchemaToZod } from "x-to-zod";const schema = {
type: 'object',
properties: {
name: {
type: 'string',
'x-custom-min': 5, // Vendor-specific extension
'x-custom-max': 100
}
}
};
const code = jsonSchemaToZod(schema, {
preprocessors: [
(schema, refs) => {
// Convert custom extensions to standard JSON schema
if (schema['x-custom-min'] !== undefined) {
return {
...schema,
minLength: schema['x-custom-min'],
maxLength: schema['x-custom-max']
};
}
}
]
});
// Output includes: z.string().min(5).max(100)
`#### Example: Strip Unsupported Keywords
`typescript
const code = jsonSchemaToZod(schema, {
preprocessors: [
(schema, refs) => {
const { $comment, examples, ...rest } = schema;
// Remove keywords not supported by Zod
return rest;
}
]
});
`#### Example: Inject Constraints Based on Path
`typescript
const code = jsonSchemaToZod(schema, {
preprocessors: [
(schema, refs) => {
// Add minimum length to all string properties under 'user'
if (
refs.path[0] === 'properties' &&
refs.path[1] === 'user' &&
schema.type === 'string' &&
!schema.minLength
) {
return {
...schema,
minLength: 1 // Ensure non-empty strings
};
}
}
]
});
`#### Example: Multiple Preprocessors
Preprocessors are executed in order, allowing you to chain transformations:
`typescript
const code = jsonSchemaToZod(schema, {
preprocessors: [
// First: normalize vendor extensions
(schema) => {
if (schema['x-nullable']) {
return { ...schema, nullable: true };
}
},
// Second: apply default constraints
(schema) => {
if (schema.type === 'string' && !schema.maxLength) {
return { ...schema, maxLength: 1000 };
}
},
// Third: remove internal metadata
(schema) => {
const { 'x-internal-id': _, ...rest } = schema;
return rest;
}
]
});
`$3
Use both options together for maximum flexibility:
`typescript
const code = jsonSchemaToZod(schema, {
preprocessors: [
// Transform schema structure first
(schema) => {
if (schema['x-custom-type']) {
return { type: schema['x-custom-type'], ...schema };
}
}
],
parserOverride: (schema, refs) => {
// Then override code generation for specific cases
if (schema.type === 'custom-type') {
return 'myCustomTypeValidator';
}
}
});
`Documentation
$3
- Parser Architecture - Understanding the class-based parser system
- Class hierarchy and template method pattern
- Parser selection algorithm
- Type guards and symmetric parse API
- How to add new parser classes
- Post-Processing Guide - Transform builders after parsing
- Post-processor concepts and use cases
- Type and path filtering
- Practical examples (strict objects, non-empty arrays, custom validations)
- Best practices and debugging tips
- Migration Guide - For contributors extending parsers
- Functional vs class-based comparison
- Migration steps and patterns
- Testing strategies
- FAQs
- API Reference - Complete API documentation
- BaseParser and all parser classes
- Registry functions and parse API
- Type definitions and type guards
- Usage examples
$3
- Builder API - Fluent builder interface
- CLI Options - Command-line usage
- Programmatic Usage - Using in code
- Post-Processing - Custom transformations
Important Notes
$3
Factored schemas (like object schemas with "oneOf" etc.) is only partially supported. Here be dragons.
$3
The output of this package is not meant to be used at runtime. JSON Schema and Zod does not overlap 100% and the scope of the parsers are purposefully limited in order to help the author avoid a permanent state of chaotic insanity. As this may cause some details of the original schema to be lost in translation, it is instead recommended to use tools such as Ajv to validate your runtime values directly against the original JSON Schema.
That said, it's possible in most cases to use
eval. Here's an example that you shouldn't use:`typescript
const zodSchema = eval(jsonSchemaToZod({ type: "string" }, { module: "cjs" }));zodSchema.safeParse("Please just use Ajv instead");
``This is a fork of json-schema-to-zod by Stefan Terdell.
JSON Schema TypeScript type definitions provided by json-schema-typed by Thomas Aribart, supporting JSON Schema draft-2020-12.
Original contributors include:
- Chen (https://github.com/werifu)
- Nuno Carduso (https://github.com/ncardoso-barracuda)
- Lars Strojny (https://github.com/lstrojny)
- Navtoj Chahal (https://github.com/navtoj)
- Ben McCann (https://github.com/benmccann)
- Dmitry Zakharov (https://github.com/DZakh)
- Michel Turpin (https://github.com/grimly)
- David Barratt (https://github.com/davidbarratt)
- pevisscher (https://github.com/pevisscher)
- Aidin Abedi (https://github.com/aidinabedi)
- Brett Zamir (https://github.com/brettz9)
- vForgeOne (https://github.com/vforgeone)
- Adrian Ordonez (https://github.com/adrianord)
- Jonas Reucher (https://github.com/Mantls)
ISC