JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 682
  • Score
    100M100P100Q16636F
  • License MIT

Bidirectional conversion between Zod schemas and TypeScript classes with class-transformer and class-validator decorators. 100% type preservation and round-trip conversion support.

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

npm GitHub

Complete bridge between JSON Schema, Zod schemas, and TypeScript classes. JSON Schema ↔ Zod ↔ Class with 100% type preservation!

npm | GitHub

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-metadata

For Swagger support (optional):

npm install @nestjs/swagger

For GraphQL support (optional):

npm install @nestjs/graphql

Quick 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 jsonSchema

Supported 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 schema
  • options - Configuration options:
    • name?: string - Name for the top-level definition
    • target?: '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 $ref resolver

Returns: A Zod schema (z.ZodTypeAny)

zodToClass(schema, options)

Convert a Zod schema to a runtime TypeScript class.

Parameters:

  • schema - A Zod schema (must be z.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/swagger decorators (default: false)
    • includeGraphQL?: boolean - Include @nestjs/graphql decorators (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 metadata
  • options - 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 class

Arrays

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 items

Enums

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 decorator

Optional 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 identically

What'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):

  1. static attributeTypeMap — OpenAPI codegen convention. Provides type, format, description, and modelClass per property. This is the richest source and enables correct Int vs Float mapping (via format: "int32"), descriptions, and nested class resolution.
  2. @Type(() => X) decorators — class-transformer metadata for nested classes and arrays.
  3. design:type reflect-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)

  1. Root must be z.object(): The root schema must be a Zod object schema. Primitive types at the root are not supported.

  2. z.refine() and z.transform(): Custom refinements and transformations cannot be converted to decorators. These are ignored during conversion.

  3. Recursive schemas: z.lazy() for recursive schemas has limited support. Deep circular references may cause issues.

  4. z.record() and z.map(): These are converted to generic TypeScript types (Record<string, any>, Map<any, any>) without specific validators.

  5. z.intersection(): Intersection types are simplified by merging properties. Complex intersections may not work as expected.

  6. z.union(): Non-discriminated unions have limited support. Use z.discriminatedUnion() for best results.

classToZod (Class → Zod)

  1. Requires decorator metadata: Classes must have class-validator decorators. Properties without decorators will use design:type metadata or default to z.unknown().

  2. Custom validators: Custom class-validator decorators (created with @ValidatorConstraint()) cannot be converted to Zod.

  3. 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