x-to-zod
v0.10.2
Published
Enhanced fork of json-schema-to-zod - Converts JSON Schema into Zod schemas with fluent builder pattern
Downloads
699
Readme
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.
Overview
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
Enhancements in This Fork
This fork includes several architectural improvements and new features:
1. Fluent Builder Pattern Architecture
- Complete rewrite of internal code generation using fluent builders
- Consistent lazy evaluation pattern across all schema types
- Builder classes mirror Zod's API:
build.number(),build.string(),build.object(), etc. - Smart constraint merging (e.g., multiple
.min()calls keep the strictest value)
2. Consolidated Modifier System
- All builders extend
BaseBuilder<T>with shared modifiers - Eliminated 154 lines of duplicated code
- Consistent behavior for
.optional(),.nullable(),.default(),.describe(),.brand(),.readonly(),.catch(), and more
3. Enhanced Zod v4 Support
- Support for new Zod v4 types:
void,undefined,date,bigint,symbol,nan - String validators:
url,httpUrl,hostname,emoji,base64url,hex,jwt,nanoid,cuid,cuid2,ulid,ipv4,ipv6,mac,cidrv4,cidrv6,hash,isoDate,isoTime,isoDatetime,isoDuration,uuidv4,uuidv6,uuidv7 - Collection types:
set,map - Advanced types:
promise,lazy,function,codec,preprocess,pipe,json,file,nativeEnum,templateLiteral,xor,keyof
4. Improved oneOf Handling
- Simplified implementation using native
z.xor()instead of manual superRefine - Cleaner generated code for exclusive OR (exactly one schema must match)
5. Better Module Resolution
- Fixed ESM build configuration for proper TypeScript module resolution
- Added
moduleResolution: "bundler"for compatibility with modern bundlers
6. Type-Safe Builder Params
- All Zod v4 builder factory functions accept properly typed params instead of
unknown - String format builders (
hex,hostname,jwt,mac,xid,ksuid,e164,base64url,httpUrl) acceptParameters<typeof z.X>[0] - Number format builders (
int,float32,float64,int32,uint32) and BigInt builders (int64,uint64) fully typed - Full IntelliSense support — error options, constraints, and all Zod params are visible in IDE
7. Code Quality
- Migrated to Vitest for faster test execution
- Updated linting with oxlint
- Better test coverage and organization
- 90-test symmetry suite covering the full
buildV4factory
8. Zod v3/v4 Dual-Mode Support
- Generate schemas compatible with either Zod v3 or v4 via
zodVersionoption - Defaults to
'v3'for backward compatibility - v4 mode generates new syntax:
z.strictObject(),z.looseObject(),.extend()instead of.merge() - Fully backward compatible - existing code continues to work without changes
9. Post-Processing System
- Transform Zod builders after parsing with custom post-processors
- Type-based filtering to target specific builder types (objects, arrays, strings, etc.)
- Path-based filtering for granular control
- Use cases: add organization-wide validation rules, security constraints, custom transformations
- See Post-Processing Guide for details
Installation
npm i -g x-to-zodUsage
Version-Specific Imports
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)
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)
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):
import { build } from 'x-to-zod';
// Same as 'x-to-zod/v4'CLI
Simplest example
x-to-zod -i mySchema.json -o mySchema.tsExample with $refs resolved and output formatted
npm i -g x-to-zod json-refs prettier
```console
json-refs resolve mySchema.json | x-to-zod | prettier --parser typescript > mySchema.tsOptions
| 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. |
Programmatic
Simple example
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);Post-Processing
Transform Zod builders after parsing with post-processors:
typeFilter matches parser typeKind values (for example object, array, oneOf).
It does not match builder class names such as ObjectBuilder.
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.
Multi-Schema Projects
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
Quick Example
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.tsCLI Project Mode
# Basic usage
x-to-zod --project --schemas "./schemas/*.json" --out ./generated
# With options
x-to-zod --project \
--schemas "./schemas/users/*.json" \
--schemas "./schemas/posts/*.json" \
--out ./generated \
--module-format both \
--zod-version v4 \
--generate-indexSee 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)
- Helpers:
build.array(item)→z.array(item)- Constraints:
.min(n),.max(n),.length(n),.nonempty() nonempty()delegates to.min(1)and accepts the same typed paramslength(n)sets an exact size and supersedes anymin/maxon serialization
- Constraints:
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(params?)→z.json()— typed params for error messages
Zod v4 Format Builders (all accept typed params for error customisation):
- String:
build.hex(params?),build.hostname(params?),build.jwt(params?),build.mac(params?),build.e164(params?),build.xid(params?),build.ksuid(params?),build.base64url(params?),build.httpUrl(params?) - Number:
build.int(params?),build.float32(params?),build.float64(params?),build.int32(params?),build.uint32(params?) - BigInt:
build.int64(params?),build.uint64(params?)
- String:
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)
Examples
// 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
import { z } from "zod";
export default z.object({ hello: z.string().optional() });moduleWithType
import { z } from "zod";
export const mySchema = z.object({ hello: z.string().optional() });
export type MySchema = z.infer<typeof mySchema>;cjs
const { z } = require("zod");
module.exports = { mySchema: z.object({ hello: z.string().optional() }) };justTheSchema
z.object({ hello: z.string().optional() });Example with $refs resolved and output formatted
import { z } from "zod";
import { resolveRefs } from "json-refs";
import { format } from "prettier";
import jsonSchemaToZod from "x-to-zod";
async function example(jsonSchema: Record<string, unknown>): Promise<string> {
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.
Basic Usage
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' });Key Differences
The zodVersion option affects how certain Zod constructs are generated:
Object Strict/Loose Modes
v3 mode (default):
// additionalProperties: false
z.object({ name: z.string() }).strict()
// passthrough behavior (using .loose() method)
z.object({ name: z.string() }).passthrough()v4 mode:
// additionalProperties: false
z.strictObject({ name: z.string() })
// passthrough behavior
z.looseObject({ name: z.string() })Object Merge
v3 mode (default):
baseObject.merge(otherObject)v4 mode:
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" }
Builder API with Version Support
The builder API also respects the zodVersion option:
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()'Migration Guide
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
- Start with v3 mode (default) - your existing code continues to work
- Test thoroughly - ensure all generated schemas work as expected
- Switch to v4 mode - set
zodVersion: 'v4'when ready - Update consuming code - adjust for any Zod v4 API changes
- Enjoy improved syntax - benefit from cleaner, more concise schemas
Compatibility Notes
- 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
Custom Parsers
Register custom parsers for schema types not handled by the built-in parsers. Custom parsers are the primary extension point for customizing code generation.
Using registerParser()
import { registerParser, AbstractParser, jsonSchemaToZod } from "x-to-zod";
// Define a custom parser class
class CustomDateTimeParser extends AbstractParser<object, 'custom-datetime'> {
readonly typeKind = 'custom-datetime' as const;
protected parseImpl(schema) {
return this.refs.build.code('z.string().datetime()');
}
}
// Register it globally
registerParser('custom-datetime', CustomDateTimeParser);
// Now schemas with type: 'custom-datetime' dispatch to your parser
const code = jsonSchemaToZod({ type: 'custom-datetime' } as any);
// Output: z.string().datetime()Custom parsers extend AbstractParser and receive full access to:
this.schema— the current schema nodethis.refs— context with path tracking, build factory, and optionsthis.parseChild(schema, ...path)— recursive parsing of child schemas- Automatic
transformersandpostProcessorspipeline integration
Schema Transformers
The transformers option transforms JSON schema nodes before parsing. Use this for:
- Normalizing vendor-specific schema extensions
- Applying global schema transformations
- Removing or modifying unsupported keywords
- Injecting default values or constraints
Function Signature
import type { SchemaTransformer, Context } from 'x-to-zod';
// SchemaTransformer is a callable with an optional pathPattern property, for example:
const transformer: SchemaTransformer = (schema: object, refs: Context): object | undefined => {
// perform any transformation you need here
return schema;
};
transformer.pathPattern = ['/paths/*'];Return Values:
object: The transformed schema (replaces the original)undefined: Keep the schema unchanged
Example: Normalize Vendor Extensions
import { jsonSchemaToZod } from "x-to-zod";
const schema = {
type: 'object',
properties: {
name: {
type: 'string',
'x-custom-min': 5,
'x-custom-max': 100
}
}
};
const code = jsonSchemaToZod(schema, {
transformers: [
(schema, refs) => {
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: Multiple Transformers with Path Filtering
Transformers execute in order and support pathPattern for targeted application:
const code = jsonSchemaToZod(schema, {
transformers: [
// Normalize vendor extensions everywhere
(schema) => {
if (schema['x-nullable']) {
return { ...schema, nullable: true };
}
},
// Apply default constraints to strings
Object.assign(
(schema) => {
if (schema.type === 'string' && !schema.maxLength) {
return { ...schema, maxLength: 1000 };
}
},
{ pathPattern: 'properties.*' } // Only under properties
),
// Strip unsupported keywords
(schema) => {
const { $comment, examples, ...rest } = schema;
return rest;
}
]
});Combining Transformers and Post-Processors
The full pipeline runs in this order:
Schema → transformers → Parser → postProcessors → Builder
(Schema→Schema) (Builder→Builder)Use transformers to modify schemas before parsing and postProcessors to modify builders after parsing:
const code = jsonSchemaToZod(schema, {
// Transform schema structure first
transformers: [
(schema) => {
if (schema['x-custom-type']) {
return { type: schema['x-custom-type'], ...schema };
}
}
],
// Then modify the generated builder
postProcessors: [
{
processor: (builder, context) => {
if ((context.schema as any).type === 'custom-type') {
return context.build.any();
}
},
pathPattern: 'properties.*'
}
]
});Migration from Previous Versions
If you were using parserOverride or preprocessors, update your code:
| Before | After |
|--------|-------|
| parserOverride: fn | Use registerParser() for type-based overrides, or postProcessors with pathPattern for path-specific overrides |
| preprocessors: [...] | transformers: [...] (same signature, renamed) |
| preProcessors: [...] | transformers: [...] (same signature, renamed) |
Documentation
Comprehensive Guides
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
Quick Links
- Builder API - Fluent builder interface
- CLI Options - Command-line usage
- Programmatic Usage - Using in code
- Post-Processing - Custom transformations
Important Notes
Schema Factoring
Factored schemas (like object schemas with "oneOf" etc.) is only partially supported. Here be dragons.
Use at Runtime
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:
const zodSchema = eval(jsonSchemaToZod({ type: "string" }, { module: "cjs" }));
zodSchema.safeParse("Please just use Ajv instead");Credits
This is a fork of json-schema-to-zod by Stefan Terdell.
Type Definitions
JSON Schema TypeScript type definitions provided by json-schema-typed by Thomas Aribart, supporting JSON Schema draft-2020-12.
Original Contributors
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)
License
ISC
