@hiscojs/zodify
v0.1.0
Published
Bidirectional conversion between Zod schemas and TypeScript classes with class-transformer and class-validator decorators. 100% type preservation and round-trip conversion support.
Maintainers
Readme
zodify
Bidirectional conversion between Zod schemas and TypeScript classes with class-transformer and class-validator decorators. 100% type preservation in both directions!
Features
- ✅ Bidirectional conversion: Zod ↔ Class with zero data loss
- ✅ 100% round-trip preservation: Convert Zod → Class → Zod with all constraints intact
- ✅ Array constraints: Full support for
.min(),.max(),.length(),.nonempty() - ✅ Primitive array items: Preserve constraints like
z.array(z.string().min(2)) - ✅ Date ranges:
z.date().min(date)and.max(date)support - ✅ String patterns:
startsWith(),endsWith()with regex escaping - ✅ Deep nesting: Tested up to 5 levels with full validation
- ✅ Runtime class generation: Dynamic class creation from Zod schemas
- ✅ Code generation: Generate TypeScript code strings
- ✅ NestJS ready: Perfect for DTOs and validation pipes
Installation
npm install zodify zod class-transformer class-validator reflect-metadataQuick Start - Zod to Class
import 'reflect-metadata';
import { z } from 'zod';
import { zodToClass } from 'zodify';
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
// Define a Zod schema
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
age: z.number().int().min(0).max(120),
role: z.enum(['admin', 'user', 'guest']),
});
// Generate a runtime class
const UserClass = zodToClass(UserSchema, { className: 'User' });
// Use with class-validator
const user = plainToInstance(UserClass, {
id: '123e4567-e89b-12d3-a456-426614174000',
email: '[email protected]',
age: 25,
role: 'admin',
});
const errors = await validate(user);
console.log(errors); // []Quick Start - Class to Zod
import 'reflect-metadata';
import { classToZod } from 'zodify';
import { IsString, IsEmail, IsInt, Min, Max, IsEnum } from 'class-validator';
import { Expose } from 'class-transformer';
// Define a class with decorators
class User {
@IsString()
@Expose()
id: string;
@IsString()
@IsEmail()
@Expose()
email: string;
@IsInt()
@Min(0)
@Max(120)
@Expose()
age: number;
@IsEnum(['admin', 'user', 'guest'])
@Expose()
role: string;
}
// Convert to Zod schema
const UserSchema = classToZod(User);
// Use with Zod validation
const user = UserSchema.parse({
id: '123e4567-e89b-12d3-a456-426614174000',
email: '[email protected]',
age: 25,
role: 'admin',
});API Reference
zodToClass(schema, options)
Convert a Zod schema to a runtime TypeScript class.
Parameters:
schema- A Zod schema (must bez.object()at the root level)options- Configuration options:className?: string- Generated class name (default: 'GeneratedClass')includeValidators?: boolean- Include class-validator decorators (default: true)includeTransformers?: boolean- Include class-transformer decorators (default: true)exportClass?: boolean- Export the class (for code generation, default: true)includeImports?: boolean- Add import statements (for code generation, default: true)
Returns: A JavaScript class constructor
Example:
const UserClass = zodToClass(UserSchema, {
className: 'User',
includeValidators: true,
includeTransformers: true,
});
const user = new UserClass({ name: 'John', email: '[email protected]' });zodToClass.toCode(schema, options)
Generate TypeScript code string from a Zod schema.
Parameters: Same as zodToClass()
Returns: A string containing TypeScript code
Example:
const code = zodToClass.toCode(UserSchema, { className: 'User' });
console.log(code);Output:
import { Expose } from 'class-transformer';
import { IsString, IsEmail } from 'class-validator';
export class User {
@IsString()
@Expose()
name: string;
@IsString()
@IsEmail()
@Expose()
email: string;
}classToZod(classConstructor, options)
Convert a class with class-validator and class-transformer decorators to a Zod schema.
Parameters:
classConstructor- A class constructor with decorator metadataoptions- Configuration options:useReflectMetadata?: boolean- Use reflect-metadata for type inference (default: true)strict?: boolean- Throw on unknown properties (default: false)classCache?: WeakMap- Cache for circular references (default: new WeakMap())
Returns: A Zod schema (z.ZodObject)
Example:
import { IsString, IsEmail } from 'class-validator';
import { Expose } from 'class-transformer';
import { classToZod } from 'zodify';
class User {
@IsString()
@IsEmail()
@Expose()
email: string;
@IsString()
@Expose()
name: string;
}
const schema = classToZod(User);
// Returns: z.object({ email: z.string().email(), name: z.string() })
// Use with Zod validation
const user = schema.parse({ email: '[email protected]', name: 'John' });Decorator to Zod Mapping
class-validator to Zod
Type Validators
| class-validator Decorator | Zod Schema |
|---------------------------|------------|
| @IsString() | z.string() |
| @IsNumber() | z.number() |
| @IsInt() | z.number().int() |
| @IsBoolean() | z.boolean() |
| @IsDate() | z.date() |
| @IsEnum(Enum) | z.enum([...]) or z.nativeEnum(Enum) |
| @IsArray() | z.array(...) |
| @IsOptional() | .optional() |
String Validators
| class-validator Decorator | Zod Schema |
|---------------------------|------------|
| @IsEmail() | z.string().email() |
| @IsUUID() | z.string().uuid() |
| @IsUrl() | z.string().url() |
| @IsIP() | z.string().ip() |
| @IsDateString() | z.string().datetime() |
| @MinLength(n) | z.string().min(n) |
| @MaxLength(n) | z.string().max(n) |
| @Length(n, n) | z.string().length(n) |
| @Contains(str) | z.string().includes(str) |
| @Matches(regex) | z.string().regex(regex) |
Number Validators
| class-validator Decorator | Zod Schema |
|---------------------------|------------|
| @Min(n) | z.number().min(n) |
| @Max(n) | z.number().max(n) |
| @IsPositive() | z.number().positive() |
| @IsNegative() | z.number().negative() |
| @IsDivisibleBy(n) | z.number().multipleOf(n) |
Array Validators
| class-validator Decorator | Zod Schema |
|---------------------------|------------|
| @ArrayMinSize(n) | z.array(...).min(n) |
| @ArrayMaxSize(n) | z.array(...).max(n) |
| @ArrayNotEmpty() | z.array(...).nonempty() |
Date Validators
| class-validator Decorator | Zod Schema |
|---------------------------|------------|
| @MinDate(date) | z.date().min(date) |
| @MaxDate(date) | z.date().max(date) |
Zod to Decorator Mapping
class-validator Decorators
Type Validators
| Zod Schema | class-validator Decorator |
|------------|---------------------------|
| z.string() | @IsString() |
| z.number() | @IsNumber() |
| z.number().int() | @IsInt() |
| z.boolean() | @IsBoolean() |
| z.date() | @IsDate() |
| z.enum([...]) | @IsEnum(EnumType) |
| z.array(T) | @IsArray() |
| z.optional() | @IsOptional() |
String Validators
| Zod Schema | class-validator Decorator |
|------------|---------------------------|
| z.string().email() | @IsEmail() |
| z.string().uuid() | @IsUUID() |
| z.string().url() | @IsUrl() |
| z.string().cuid() | @Matches(/^c[^\s-]{8,}$/i) |
| z.string().cuid2() | @Matches(/^[0-9a-z]+$/) |
| z.string().datetime() | @IsDateString() |
| z.string().ip() | @IsIP() |
| z.string().min(n) | @MinLength(n) |
| z.string().max(n) | @MaxLength(n) |
| z.string().length(n) | @Length(n, n) |
| z.string().includes(str) | @Contains(str) |
| z.string().regex(pattern) | @Matches(pattern) |
| z.string().startsWith('prefix') | @Matches(/^prefix/) |
| z.string().endsWith('suffix') | @Matches(/suffix$/) |
Number Validators
| Zod Schema | class-validator Decorator |
|------------|---------------------------|
| z.number().min(n) | @Min(n) |
| z.number().max(n) | @Max(n) |
| z.number().positive() | @IsPositive() |
| z.number().negative() | @IsNegative() |
| z.number().multipleOf(n) | @IsDivisibleBy(n) |
Array Validators
| Zod Schema | class-validator Decorator |
|------------|---------------------------|
| z.array(T).min(n) | @ArrayMinSize(n) |
| z.array(T).max(n) | @ArrayMaxSize(n) |
| z.array(T).length(n) | @ArrayMinSize(n) + @ArrayMaxSize(n) |
| z.array(T).nonempty() | @ArrayNotEmpty() |
Date Validators
| Zod Schema | class-validator Decorator |
|------------|---------------------------|
| z.date().min(date) | @MinDate(date) |
| z.date().max(date) | @MaxDate(date) |
class-transformer Decorators
| Zod Schema | class-transformer Decorator |
|------------|----------------------------|
| All properties | @Expose() |
| z.object(...) | @Type(() => NestedClass) |
| z.array(z.object(...)) | @Type(() => ItemClass) |
| z.date() | @Type(() => Date) |
| z.discriminatedUnion(...) | @Type(() => Object, { discriminator: {...} }) |
Usage Examples
Basic Schema
const UserSchema = z.object({
name: z.string(),
email: z.string().email(),
age: z.number().int().min(0).max(120),
});
const UserClass = zodToClass(UserSchema, { className: 'User' });Nested Objects
const UserSchema = z.object({
name: z.string(),
address: z.object({
street: z.string(),
city: z.string(),
zipCode: z.string(),
}),
});
const UserClass = zodToClass(UserSchema, { className: 'User' });
// Creates User class and UserAddress nested classArrays
const UserSchema = z.object({
name: z.string(),
tags: z.array(z.string()),
posts: z.array(z.object({
title: z.string(),
content: z.string(),
})),
});
const UserClass = zodToClass(UserSchema, { className: 'User' });
// Creates User class and UserPostsItem nested class for array itemsEnums
const UserSchema = z.object({
name: z.string(),
role: z.enum(['admin', 'user', 'guest']),
});
const UserClass = zodToClass(UserSchema, { className: 'User' });Discriminated Unions
const ComponentSchema = z.object({
component: z.discriminatedUnion('type', [
z.object({
type: z.literal('helm'),
chartName: z.string(),
version: z.string(),
}),
z.object({
type: z.literal('kustomize'),
path: z.string(),
}),
z.object({
type: z.literal('manifest'),
yaml: z.string(),
}),
]),
});
const ComponentClass = zodToClass(ComponentSchema, { className: 'Component' });
// Creates separate classes for each variant with discriminator decoratorOptional and Nullable
const UserSchema = z.object({
name: z.string(),
nickname: z.string().optional(),
middleName: z.string().nullable(),
bio: z.string().optional().nullable(),
});
const UserClass = zodToClass(UserSchema, { className: 'User' });Round-trip Conversion
zodify preserves 100% of your constraints in round-trip conversions:
// Start with a Zod schema
const originalSchema = z.object({
email: z.string().email(),
age: z.number().int().min(18).max(120),
tags: z.array(z.string()).min(1).max(10),
startDate: z.date().min(new Date('2024-01-01')),
});
// Convert to class
const UserClass = zodToClass(originalSchema, { className: 'User' });
// Convert back to Zod
const regeneratedSchema = classToZod(UserClass);
// All constraints are preserved!
originalSchema.parse(data); // ✅ Validates correctly
regeneratedSchema.parse(data); // ✅ Validates identicallyWhat's preserved:
- ✅ All type validators (string, number, boolean, date, array, etc.)
- ✅ All constraint validators (min, max, length, email, uuid, etc.)
- ✅ Array length constraints (min, max, length, nonempty)
- ✅ Date range constraints (min, max)
- ✅ String patterns (startsWith, endsWith, regex)
- ✅ Nested objects (unlimited depth)
- ✅ Optional and nullable properties
- ✅ Enums and discriminated unions
- ✅ Even primitive array item constraints like
z.array(z.string().min(2))
Code Generation
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
age: z.number().int().min(0).max(120),
});
// Generate code string
const code = zodToClass.toCode(UserSchema, {
className: 'User',
includeImports: true,
exportClass: true,
});
// Write to file or use programmatically
import fs from 'fs';
fs.writeFileSync('./generated/user.dto.ts', code);NestJS Integration
zodify works seamlessly with NestJS applications.
import { z } from 'zod';
import { zodToClass } from 'zodify';
import { Controller, Post, Body } from '@nestjs/common';
import { ValidationPipe } from '@nestjs/common';
// Define Zod schema
const CreateUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string(),
});
// Generate DTO class
const CreateUserDto = zodToClass(CreateUserSchema, { className: 'CreateUserDto' });
@Controller('users')
export class UsersController {
@Post()
async create(@Body() createUserDto: typeof CreateUserDto) {
// createUserDto is already validated and transformed!
return { success: true };
}
}
// Enable validation in main.ts
app.useGlobalPipes(new ValidationPipe({
transform: true,
whitelist: true,
}));TypeScript Configuration
Ensure your tsconfig.json has decorators enabled:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Limitations
zodify has some limitations:
zodToClass (Zod → Class)
Root must be z.object(): The root schema must be a Zod object schema. Primitive types at the root are not supported.
z.refine() and z.transform(): Custom refinements and transformations cannot be converted to decorators. These are ignored during conversion.
Recursive schemas:
z.lazy()for recursive schemas has limited support. Deep circular references may cause issues.z.record() and z.map(): These are converted to generic TypeScript types (
Record<string, any>,Map<any, any>) without specific validators.z.intersection(): Intersection types are simplified by merging properties. Complex intersections may not work as expected.
z.union(): Non-discriminated unions have limited support. Use
z.discriminatedUnion()for best results.
classToZod (Class → Zod)
Requires decorator metadata: Classes must have class-validator decorators. Properties without decorators will use
design:typemetadata or default toz.unknown().Custom validators: Custom class-validator decorators (created with
@ValidatorConstraint()) cannot be converted to Zod.Primitive array item constraints: For manually written classes, constraints on primitive array items (e.g., "each string must be at least 2 chars") cannot be expressed with class-validator decorators alone. However, this works perfectly for round-trip conversions (Zod → Class → Zod) using metadata storage.
Both Directions
- z.tuple(): Tuple types are not yet supported.
- z.promise() and z.function(): These types cannot be represented in decorators.
- z.default(): Default values are not preserved during conversion.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT
Related Projects
- zod - TypeScript-first schema validation
- class-validator - Decorator-based validation
- class-transformer - Decorator-based transformation
- klasik - Generate TypeScript clients from OpenAPI specs
