Package Exports
- @hiscojs/zodify
- @hiscojs/zodify/dist/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@hiscojs/zodify) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
zodify
Complete bridge between JSON Schema, Zod schemas, and TypeScript classes. JSON Schema ↔ Zod ↔ Class with 100% type preservation!
Features
- ✅ JSON Schema ↔ Zod: Convert between JSON Schema (Draft-07) and runtime Zod schemas
- ✅ Bidirectional conversion: Zod ↔ Class with zero data loss
- ✅ 100% round-trip preservation: Convert Zod → Class → Zod with all constraints intact
- ✅ Swagger / OpenAPI: Auto-generate
@ApiProperty()/@ApiPropertyOptional()from@nestjs/swagger - ✅ GraphQL: Auto-generate
@Field(),@ObjectType()/@InputType()from@nestjs/graphql - ✅
asGraphQLType(): Add GraphQL decorators to existing classes at runtime (ideal for OpenAPI-generated models) - ✅ 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, Swagger docs, and GraphQL resolvers
Installation
npm install zodify zod class-transformer class-validator reflect-metadataFor Swagger support (optional):
npm install @nestjs/swaggerFor GraphQL support (optional):
npm install @nestjs/graphqlQuick 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: 'test@example.com',
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: 'test@example.com',
age: 25,
role: 'admin',
});Quick Start - JSON Schema ↔ Zod
import { zodToJsonSchema, jsonSchemaToZod } from 'zodify';
import { z } from 'zod';
// Zod → JSON Schema
const UserSchema = z.object({
email: z.string().email(),
age: z.number().int().min(0).max(120),
role: z.enum(['admin', 'user', 'guest']),
});
const jsonSchema = zodToJsonSchema(UserSchema);
// { type: "object", properties: { email: { type: "string", format: "email" }, ... } }
// JSON Schema → Zod
const zodSchema = jsonSchemaToZod(jsonSchema);
zodSchema.parse({ email: 'test@example.com', age: 25, role: 'admin' }); // ✅
// Round-trip: JSON Schema → Zod → JSON Schema
const roundTripped = zodToJsonSchema(jsonSchemaToZod(jsonSchema));
// Structurally equivalent to the original jsonSchemaSupported JSON Schema Features
| JSON Schema | Zod |
|---|---|
type: "string" |
z.string() |
type: "number" |
z.number() |
type: "integer" |
z.number().int() |
type: "boolean" |
z.boolean() |
type: "object" + properties |
z.object({...}) |
type: "array" + items |
z.array(...) |
enum: [...] |
z.enum([...]) |
additionalProperties |
z.record(...) |
$ref + definitions/$defs |
Resolved recursively |
oneOf/anyOf |
z.union([...]) |
oneOf: [X, {type:"null"}] |
.nullable() |
nullable: true (OpenAPI) |
.nullable() |
Not in required |
.optional() |
description |
.describe() |
default |
.default() |
String formats: email, uri/url, uuid, date-time, ipv4, ipv6
String constraints: minLength, maxLength, pattern
Number constraints: minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf
Array constraints: minItems, maxItems
API Reference
zodToJsonSchema(schema, options)
Convert a Zod schema to a JSON Schema (Draft-07 by default).
Parameters:
schema- Any Zod schemaoptions- Configuration options:name?: string- Name for the top-level definitiontarget?: 'draft-07' | 'draft-2019-09' | 'openApi3'- Target JSON Schema draft (default: 'draft-07')
Returns: A JSON Schema object
jsonSchemaToZod(jsonSchema, options)
Convert a JSON Schema object to a runtime Zod schema.
Parameters:
jsonSchema- A JSON Schema object (Draft-04/07 compatible)options- Configuration options:refResolver?: (ref, rootSchema) => JsonSchema- Custom$refresolver
Returns: A Zod schema (z.ZodTypeAny)
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)includeSwagger?: boolean- Include@nestjs/swaggerdecorators (default: false)includeGraphQL?: boolean- Include@nestjs/graphqldecorators (default: false)graphqlType?: 'ObjectType' | 'InputType'- GraphQL class decorator type (default: 'ObjectType')registry?: SchemaRegistry- Schema registry for cross-type references (see SchemaRegistry)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: 'john@example.com' });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: 'test@example.com', 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);Swagger / OpenAPI Support
Generate @ApiProperty() and @ApiPropertyOptional() decorators automatically from your Zod schemas. Requires @nestjs/swagger as a peer dependency.
const UserSchema = z.object({
id: z.string().uuid().describe('Unique user identifier'),
email: z.string().email().describe('User email address'),
age: z.number().int().min(0).max(120),
role: z.enum(['admin', 'user', 'guest']),
nickname: z.string().optional(),
bio: z.string().nullable(),
tags: z.array(z.string()).min(1).max(10),
});
const code = zodToClass.toCode(UserSchema, {
className: 'User',
includeSwagger: true,
});Generated output:
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Expose } from 'class-transformer';
import { IsString, IsUUID, IsEmail, IsInt, Min, Max, IsEnum, IsArray, ArrayMinSize, ArrayMaxSize } from 'class-validator';
export class User {
@ApiProperty({ type: String, format: 'uuid', description: 'Unique user identifier' })
@IsString()
@IsUUID()
@Expose()
id!: string;
@ApiProperty({ type: String, format: 'email', description: 'User email address' })
@IsString()
@IsEmail()
@Expose()
email!: string;
@ApiProperty({ type: Number, minimum: 0, maximum: 120 })
@IsInt()
@Min(0)
@Max(120)
@Expose()
age!: number;
@ApiProperty({ type: String, enum: ['admin', 'user', 'guest'] })
@IsEnum(['admin', 'user', 'guest'])
@Expose()
role!: 'admin' | 'user' | 'guest';
@ApiPropertyOptional({ type: String })
@IsOptional()
@IsString()
@Expose()
nickname?: string;
@ApiProperty({ type: String, nullable: true })
@Expose()
bio!: string | null;
@ApiProperty({ type: String, isArray: true, minItems: 1, maxItems: 10 })
@IsArray()
@ArrayMinSize(1)
@ArrayMaxSize(10)
@Expose()
tags!: string[];
}Swagger Mapping Reference
The following Zod features map to @ApiProperty options:
| Zod Feature | @ApiProperty Option |
|---|---|
z.string() / z.number() / z.boolean() |
type: String / Number / Boolean |
.optional() |
Uses @ApiPropertyOptional instead |
.nullable() |
nullable: true |
.describe('...') |
description: '...' |
.default(value) |
default: value |
.min(n) / .max(n) (string) |
minLength / maxLength |
.min(n) / .max(n) (number) |
minimum / maximum |
.min(n) / .max(n) (array) |
minItems / maxItems |
.regex(pattern) |
pattern |
.email() |
format: 'email' |
.uuid() |
format: 'uuid' |
.url() |
format: 'uri' |
.datetime() |
format: 'date-time' |
.ip() |
format: 'ipv4' or 'ipv6' |
z.enum([...]) |
enum: [...] |
.multipleOf(n) |
multipleOf |
Nested z.object() |
type: () => NestedClass |
z.array(...) |
isArray: true + item type |
GraphQL Support
Generate @Field(), @ObjectType(), and @InputType() decorators from your Zod schemas. Requires @nestjs/graphql as a peer dependency.
const UserSchema = z.object({
name: z.string(),
age: z.number().int(),
score: z.number(),
active: z.boolean(),
bio: z.string().optional().describe('User biography'),
});
const code = zodToClass.toCode(UserSchema, {
className: 'User',
includeGraphQL: true,
});Generated output:
import { Field, Int, Float, ObjectType } from '@nestjs/graphql';
import { Expose } from 'class-transformer';
import { IsString, IsInt, IsNumber, IsBoolean, IsOptional } from 'class-validator';
@ObjectType()
export class User {
@Field(() => String)
@IsString()
@Expose()
name!: string;
@Field(() => Int)
@IsInt()
@Expose()
age!: number;
@Field(() => Float)
@IsNumber()
@Expose()
score!: number;
@Field(() => Boolean)
@IsBoolean()
@Expose()
active!: boolean;
@Field(() => String, { nullable: true, description: 'User biography' })
@IsOptional()
@IsString()
@Expose()
bio?: string;
}Use graphqlType: 'InputType' for mutation inputs:
const code = zodToClass.toCode(CreateUserSchema, {
className: 'CreateUserInput',
includeGraphQL: true,
graphqlType: 'InputType',
});
// Generates @InputType() instead of @ObjectType()GraphQL Mapping Reference
| Zod Type | GraphQL Type |
|---|---|
z.string() |
() => String |
z.number() |
() => Float |
z.number().int() |
() => Int |
z.boolean() |
() => Boolean |
z.date() |
() => Date |
z.enum([...]) |
() => String |
Nested z.object() |
() => NestedClass |
z.array(z.string()) |
() => [String] |
z.array(z.number().int()) |
() => [Int] |
z.array(z.object(...)) |
() => [ItemClass] |
.optional() / .nullable() |
{ nullable: true } |
.describe('...') |
{ description: '...' } |
.default(value) |
{ defaultValue: value } |
Combining Swagger + GraphQL
You can enable both at the same time:
const code = zodToClass.toCode(UserSchema, {
className: 'User',
includeSwagger: true,
includeGraphQL: true,
});
// Generates @ObjectType(), @Field(), @ApiProperty(), @IsString(), @Expose() etc.asGraphQLType() — GraphQL Decorators for Existing Classes
Add @ObjectType() and @Field() decorators to existing classes at runtime, without code generation. Ideal for OpenAPI-generated client classes that already have @Expose() and @Type() decorators.
import { Deployment } from '@myorg/api-client';
import { asGraphQLType } from 'zodify';
// Returns a subclass with GraphQL decorators — original class is untouched
const DeploymentModel = asGraphQLType(Deployment, {
name: 'DeploymentModel',
});
// Use in NestJS resolvers
@Resolver(() => DeploymentModel)
class DeploymentResolver {
@Query(() => DeploymentModel)
async deployment() { ... }
}Options
| Option | Type | Default | Description |
|---|---|---|---|
name |
string |
Class name | GraphQL type name |
type |
'ObjectType' | 'InputType' |
'ObjectType' |
Class-level decorator |
unknownScalar |
any |
— | Scalar for object/unknown fields (e.g. GraphQLJSON) |
processedClasses |
Map |
— | Shared cache to avoid decorating the same class twice |
Metadata Sources
asGraphQLType reads property info from (in priority order):
static attributeTypeMap— OpenAPI codegen convention. Providestype,format,description, andmodelClassper property. This is the richest source and enables correctIntvsFloatmapping (viaformat: "int32"), descriptions, and nested class resolution.@Type(() => X)decorators — class-transformer metadata for nested classes and arrays.design:typereflect-metadata — TypeScript compiler output, used as fallback.
Handling Unknown/Untyped Fields
Fields typed as object or without a recognized type are skipped by default (not valid GraphQL types). To include them, pass a scalar:
import GraphQLJSON from 'graphql-type-json';
const Model = asGraphQLType(MyClass, {
unknownScalar: GraphQLJSON,
});Nested Classes
Nested objects and arrays of objects are recursively processed. Each nested class gets its own @ObjectType() and @Field() decorators:
// If Deployment has `@Type(() => Address) address: Address`
// and `@Type(() => Listener) listeners: Listener[]`
// then Address and Listener are automatically decorated too.
const DeploymentModel = asGraphQLType(Deployment);SchemaRegistry
When generating classes from schemas that reference other schemas, zodify auto-generates nested class names (e.g. GatewaysViewGatewaysItem). Use SchemaRegistry to control these names by registering shared schemas upfront:
import { z } from 'zod';
import { zodToClass, SchemaRegistry } from 'zodify';
const GatewayListenerSchema = z.object({
name: z.string(),
protocol: z.string(),
port: z.number().int(),
});
const MergedGatewaySchema = z.object({
cluster: z.string(),
name: z.string(),
runtimeListeners: z.array(GatewayListenerSchema).optional(),
});
const GatewaysViewSchema = z.object({
gateways: z.array(MergedGatewaySchema),
failedClusters: z.array(z.string()),
});
// Register shared schemas with names
const registry = new SchemaRegistry();
registry.register('GatewayListener', GatewayListenerSchema);
registry.register('MergedGateway', MergedGatewaySchema);
// Code generation — references registered names instead of inlining
const code = zodToClass.toCode(GatewaysViewSchema, {
className: 'GatewaysView',
includeGraphQL: true,
registry,
});
// Output references MergedGateway and GatewayListener by name,
// without generating inline class definitions for them.For runtime usage, register schemas with their pre-generated class references (generate leaf types first):
const registry = new SchemaRegistry();
const GatewayListener = zodToClass(GatewayListenerSchema, {
className: 'GatewayListener',
includeGraphQL: true,
});
registry.register('GatewayListener', GatewayListenerSchema, GatewayListener);
const MergedGateway = zodToClass(MergedGatewaySchema, {
className: 'MergedGateway',
includeGraphQL: true,
registry,
});
registry.register('MergedGateway', MergedGatewaySchema, MergedGateway);
const GatewaysView = zodToClass(GatewaysViewSchema, {
className: 'GatewaysView',
includeGraphQL: true,
registry,
});Schemas are matched by reference identity — pass the same schema object that was used when defining the parent schema.
NestJS Integration
zodify works seamlessly with NestJS applications, including validation pipes, Swagger documentation, and GraphQL resolvers.
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().describe('User email'),
password: z.string().min(8).describe('Min 8 characters'),
name: z.string(),
});
// Generate DTO class with Swagger annotations
const CreateUserDto = zodToClass(CreateUserSchema, {
className: 'CreateUserDto',
includeSwagger: true,
});
@Controller('users')
export class UsersController {
@Post()
async create(@Body() createUserDto: typeof CreateUserDto) {
// createUserDto is validated, transformed, and Swagger-documented!
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 extracted for Swagger (
default) and GraphQL (defaultValue) annotations, but are not preserved in class-validator round-trip conversions.
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