// Note: This is a fork of the original nestjs-zod repository by BenLorantfy. The main addition is enhanced GraphQL support, addressing Issue #155.
✨ A seamless validation solution for your NestJS application ✨
createZodDto- create DTO classes from Zod schemasZodValidationPipe- validatebody/query/paramsusing Zod DTOsZodGuard- guard routes by validatingbody/query/params
(it can be useful when you want to do that before other guards)UseZodGuard- alias for@UseGuards(new ZodGuard(source, schema))ZodValidationException- BadRequestException extended with Zod errorszodToOpenAPI- create OpenAPI declarations from Zod schemas- GraphQL support ✨
@ZodObjectType- create GraphQL ObjectTypes from Zod schemas (minimal approach)@ZodInputType- create GraphQL InputTypes from Zod schemas (minimal approach)- Automatic field generation from Zod schema with descriptions
- Drop-in replacement for
@ObjectTypeand@InputTypefrom@nestjs/graphql
- OpenAPI support
@nestjs/swaggerintegration using the patchzodToOpenAPI- generate highly accurate Swagger Schema- Zod DTOs can be used in any
@nestjs/swaggerdecorator
- Extended Zod schemas for NestJS (
@nest-zod/z)- Note:
@nest-zod/zis deprecated and will not be supported soon. It is recommended to usezoddirectly. See MIGRATION.md for more information. dateStringfor dates (supports casting toDate)passwordfor passwords (more complex string rules + OpenAPI conversion)
- Note:
- Customization - change exception format easily
- Useful helpers for client side error handling (
@at7211/nestjs-zod/frontend)
npm install @at7211/nestjs-zod zod
Peer dependencies:
zod->= 3.14.3@nestjs/common->= 8.0.0(required on server side)@nestjs/core->= 8.0.0(required on server side)@nestjs/swagger->= 5.0.0(only when usingpatchNestJsSwagger)@nestjs/graphql->= 9.0.0(only when using GraphQL decorators)
All peer dependencies are marked as optional for better client side usage, but you need to install required ones when using nestjs-zod on server side.
- Creating DTO from Zod schema
- Using ZodValidationPipe
- Using ZodGuard
- Validation Exceptions
- Using ZodSerializerInterceptor
- GraphQL support ✨
- Extended Zod
- OpenAPI (Swagger) support
import { createZodDto } from '@at7211/nestjs-zod'
import { z } from 'zod'
const CredentialsSchema = z.object({
username: z.string(),
password: z.string(),
})
// class is required for using DTO as a type
class CredentialsDto extends createZodDto(CredentialsSchema) {}DTO does two things:
- Provides a schema for
ZodValidationPipe - Provides a type from Zod schema for you
@Controller('auth')
class AuthController {
// with global ZodValidationPipe (recommended)
async signIn(@Body() credentials: CredentialsDto) {}
async signIn(@Param() signInParams: SignInParamsDto) {}
async signIn(@Query() signInQuery: SignInQueryDto) {}
// with route-level ZodValidationPipe
@UsePipes(ZodValidationPipe)
async signIn(@Body() credentials: CredentialsDto) {}
}
// with controller-level ZodValidationPipe
@UsePipes(ZodValidationPipe)
@Controller('auth')
class AuthController {
async signIn(@Body() credentials: CredentialsDto) {}
}import { createZodDto } from '@at7211/nestjs-zod/dto'The validation pipe uses your Zod schema to parse data from parameter decorator.
When the data is invalid - it throws ZodValidationException.
import { ZodValidationPipe } from '@at7211/nestjs-zod'
import { APP_PIPE } from '@nestjs/core'
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ZodValidationPipe,
},
],
})
export class AppModule {}import { ZodValidationPipe } from '@at7211/nestjs-zod'
// controller-level
@UsePipes(ZodValidationPipe)
class AuthController {}
class AuthController {
// route-level
@UsePipes(ZodValidationPipe)
async signIn() {}
}Also, you can instantly pass a Schema or DTO:
import { ZodValidationPipe } from '@at7211/nestjs-zod'
import { UserDto, UserSchema } from './auth.contracts'
// using schema
@UsePipes(new ZodValidationPipe(UserSchema))
// using DTO
@UsePipes(new ZodValidationPipe(UserDto))
class AuthController {}
class AuthController {
// the same applies to route-level
async signIn() {}
}import { createZodValidationPipe } from '@at7211/nestjs-zod'
const MyZodValidationPipe = createZodValidationPipe({
// provide custom validation exception factory
createValidationException: (error: ZodError) =>
new BadRequestException('Ooops'),
})Sometimes, we need to validate user input before specific Guards. We can't use Validation Pipe since NestJS Pipes are always executed after Guards.
The solution is ZodGuard. It works just like ZodValidationPipe, except for that is doesn't transform the input.
It has 2 syntax forms:
@UseGuards(new ZodGuard('body', CredentialsSchema))@UseZodGuard('body', CredentialsSchema)
Parameters:
- The source -
'body' | 'query' | 'params' - Zod Schema or DTO (just like
ZodValidationPipe)
When the data is invalid - it throws ZodValidationException.
import { ZodGuard } from '@at7211/nestjs-zod'
// controller-level
@UseZodGuard('body', CredentialsSchema)
@UseZodGuard('params', CredentialsDto)
class MyController {}
class MyController {
// route-level
@UseZodGuard('query', CredentialsSchema)
@UseZodGuard('body', CredentialsDto)
async signIn() {}
}import { createZodGuard } from '@at7211/nestjs-zod'
const MyZodGuard = createZodGuard({
// provide custom validation exception factory
createValidationException: (error: ZodError) =>
new BadRequestException('Ooops'),
})If you don't like ZodGuard and ZodValidationPipe, you can use validate function:
import { validate } from '@at7211/nestjs-zod'
validate(wrongThing, UserDto, (zodError) => new MyException(zodError)) // throws MyException
const validatedUser = validate(
user,
UserDto,
(zodError) => new MyException(zodError)
) // returns typed value when succeedThe default server response on validation error looks like that:
{
"statusCode": 400,
"message": "Validation failed",
"errors": [
{
"code": "too_small",
"minimum": 8,
"type": "string",
"inclusive": true,
"message": "String must contain at least 8 character(s)",
"path": ["password"]
}
]
}The reason of this structure is default ZodValidationException.
You can customize the exception by creating custom nestjs-zod entities using the factories:
You can create ZodValidationException manually by providing ZodError:
const exception = new ZodValidationException(error)Also, ZodValidationException has an additional API for better usage in NestJS Exception Filters:
@Catch(ZodValidationException)
export class ZodValidationExceptionFilter implements ExceptionFilter {
catch(exception: ZodValidationException) {
exception.getZodError() // -> ZodError
}
}To ensure that a response conforms to a certain shape, you may use the ZodSerializerInterceptor interceptor.
This would be especially useful in prevent accidental data leaks.
This is similar to NestJs' @ClassSerializerInterceptor feature here
@Module({
...
providers: [
...,
{ provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor },
],
})
export class AppModule {}const UserSchema = z.object({ username: string() })
export class UserDto extends createZodDto(UserSchema) {}@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@ZodSerializerDto(UserDto)
getUser(id: number) {
return this.userService.findOne(id) // --> The native service method returns { username: string, password: string by default }
}
}In the above example, despite the userService.findOne method returns password, the password property will be stripped out thanks to the @ZodSerializerDto decorator.
You can catch serialization errors using ZodSerializationException and log them using your preferred logger.
if (exception instanceof ZodSerializationException) {
const zodError = exception.getZodError();
this.logger.error(`ZodSerializationException: ${zodError.message}`);
}See the example app here for more information.
@at7211/nestjs-zod provides powerful GraphQL integration that automatically generates GraphQL types from your Zod schemas. Choose the approach that fits your needs.
Prerequisites:
@nestjs/graphqlversion^9.0.0or higher- GraphQL driver (Apollo or Mercurius) configured
The library automatically detects your GraphQL setup - no additional configuration needed.
Perfect for quick prototyping and when you don't need custom methods:
import { createZodDto } from '@at7211/nestjs-zod'
import { z } from 'zod'
const UserSchema = z.object({
id: z.number().describe('User ID'),
name: z.string().describe('Full name'),
email: z.string().email().describe('Email address'),
})
// Creates DTO with GraphQL support + validation methods
const UserDto = createZodDto(UserSchema, {
graphql: { name: 'User' }
})
// Immediately usable
const user = UserDto.parse({ id: 1, name: 'John', email: '[email protected]' })Best for production code when you need custom methods and maximum type safety:
import { ZodObjectType, createZodDto } from '@at7211/nestjs-zod'
@ZodObjectType()
export class UserDto extends createZodDto(UserSchema) {
// ✅ Add custom methods
getDisplayName(): string {
return `${this.name} (${this.email})`
}
// ✅ Add static methods
static async findByEmail(email: string) {
// Custom logic here
}
// ✅ Full TypeScript intellisense
// this.id, this.name, this.email all available
}
// Usage in resolvers
@Resolver(() => UserDto)
export class UsersResolver {
@Query(() => UserDto)
async getUser(@Args('id') id: number): Promise<UserDto> {
const userData = await this.usersService.findOne(id)
return UserDto.parse(userData) // Full validation + type safety
}
}Use when you only need GraphQL types without validation methods:
@ZodObjectType(UserSchema, 'User')
export class UserDto {}
// ⚠️ Note: This gives you GraphQL types but no validation methodsCreate GraphQL InputTypes for mutations and form inputs:
const CreateUserSchema = z.object({
name: z.string().min(2).describe('Full name'),
email: z.string().email().describe('Email address'),
})
// Pattern 1: Simple
const CreateUserInput = createZodDto(CreateUserSchema, {
graphql: { name: 'CreateUserInput', isInput: true }
})
// Pattern 2: Class with validation methods
@ZodInputType()
export class CreateUserInput extends createZodDto(CreateUserSchema) {}
// Usage in resolver
@Mutation(() => UserDto)
async createUser(
@Args('input') input: CreateUserInput
): Promise<UserDto> {
// Input is automatically validated
const userData = CreateUserInput.parse(input)
return this.usersService.create(userData)
}The library automatically handles complex nested structures:
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
country: z.string(),
})
const UserSchema = z.object({
id: z.number(),
name: z.string(),
addresses: z.array(AddressSchema), // Array of objects
metadata: z.record(z.string()), // Key-value pairs
createdAt: z.date(), // Date fields
})
// Everything auto-generates correctly in GraphQL
@ZodObjectType()
export class UserDto extends createZodDto(UserSchema) {}// schemas/user.schema.ts
export const UserSchema = z.object({
id: z.number().describe('Unique identifier'),
name: z.string().min(2).max(50).describe('Full name'),
email: z.string().email().describe('Email address'),
createdAt: z.date(),
})
export const CreateUserSchema = UserSchema.omit({ id: true, createdAt: true })
export const UpdateUserSchema = CreateUserSchema.partial()
// dto/user.dto.ts
@ZodObjectType()
export class UserDto extends createZodDto(UserSchema) {}
@ZodInputType()
export class CreateUserInput extends createZodDto(CreateUserSchema) {}
@ZodInputType()
export class UpdateUserInput extends createZodDto(UpdateUserSchema) {}
// resolvers/users.resolver.ts
@Resolver(() => UserDto)
export class UsersResolver {
@Query(() => [UserDto])
async users(): Promise<UserDto[]> {
const users = await this.usersService.findAll()
return users.map(user => UserDto.parse(user))
}
@Mutation(() => UserDto)
async createUser(@Args('input') input: CreateUserInput): Promise<UserDto> {
const validInput = CreateUserInput.parse(input)
const user = await this.usersService.create(validInput)
return UserDto.parse(user)
}
@Mutation(() => UserDto)
async updateUser(
@Args('id') id: number,
@Args('input') input: UpdateUserInput
): Promise<UserDto> {
const validInput = UpdateUserInput.parse(input)
const user = await this.usersService.update(id, validInput)
return UserDto.parse(user)
}
}// ✅ Good for simple DTOs
const UserDto = createZodDto(UserSchema, { graphql: { name: 'User' } })// ✅ Best for real applications
@ZodObjectType()
export class UserDto extends createZodDto(UserSchema) {
// Add custom business logic here
}// ✅ Keep schemas separate from DTOs
// schemas/user.schema.ts
export const UserSchema = z.object({ ... })
// dto/user.dto.ts
@ZodObjectType()
export class UserDto extends createZodDto(UserSchema) {}const UserSchema = z.object({
id: z.number().describe('Unique user identifier'),
name: z.string().describe('User full name'),
email: z.string().email().describe('User email address'),
})Before (Traditional NestJS GraphQL):
@ObjectType('User')
export class UserDto {
@Field(() => ID)
id: number
@Field({ description: 'User full name' })
name: string
@Field({ description: 'User email address' })
email: string
}
@InputType('CreateUserInput')
export class CreateUserInput {
@Field({ description: 'User full name' })
name: string
@Field({ description: 'User email address' })
email: string
}After (With nestjs-zod):
const UserSchema = z.object({
id: z.number().describe('Unique user identifier'),
name: z.string().describe('User full name'),
email: z.string().email().describe('User email address'),
})
const CreateUserSchema = UserSchema.omit({ id: true })
@ZodObjectType()
export class UserDto extends createZodDto(UserSchema) {}
@ZodInputType()
export class CreateUserInput extends createZodDto(CreateUserSchema) {}Benefits:
- 75% less boilerplate code
- Single source of truth for validation and GraphQL
- Automatic validation in resolvers
- Better type safety
- Consistent field descriptions
Caution
@nest-zod/z is deprecated and will not be supported soon. It is recommended to use zod directly. See MIGRATION.md for more information.
@nest-zod/z provides a special version of Zod. It helps you to validate the user input more accurately by using our custom schemas and methods.
Caution
@nest-zod/z is deprecated and will not be supported soon. It is recommended to use zod directly. See MIGRATION.md for more information.
In HTTP, we always accept Dates as strings. But default Zod only has validations for full date-time strings. ZodDateString was created to address this issue.
// 1. Expect user input to be a "string" type
// 2. Expect user input to be a valid date (by using new Date)
z.dateString()
// Cast to Date instance
// (use it on end of the chain, but before "describe")
z.dateString().cast()
// Expect string in "full-date" format from RFC3339
z.dateString().format('date')
// [default format]
// Expect string in "date-time" format from RFC3339
z.dateString().format('date-time')
// Expect date to be the past
z.dateString().past()
// Expect date to be the future
z.dateString().future()
// Expect year to be greater or equal to 2000
z.dateString().minYear(2000)
// Expect year to be less or equal to 2025
z.dateString().maxYear(2025)
// Expect day to be a week day
z.dateString().weekDay()
// Expect year to be a weekend
z.dateString().weekend()Valid date format examples:
2022-05-15
Valid date-time format examples:
2022-05-02:08:33Z2022-05-02:08:33.000Z2022-05-02:08:33+00:002022-05-02:08:33-00:002022-05-02:08:33.000+00:00
Errors:
-
invalid_date_string- invalid date -
invalid_date_string_format- wrong formatPayload:
expected-'date' | 'date-time'
-
invalid_date_string_direction- not past/futurePayload:
expected-'past' | 'future'
-
invalid_date_string_day- not weekDay/weekendPayload:
expected-'weekDay' | 'weekend'
-
too_smallwithtype === 'date_string_year' -
too_bigwithtype === 'date_string_year'
Caution
@nest-zod/z is deprecated and will not be supported soon. It is recommended to use zod directly. See MIGRATION.md for more information.
ZodPassword is a string-like type, just like the ZodDateString. As you might have guessed, it's intended to help you with password schemas definition.
Also, ZodPassword has a more accurate OpenAPI conversion, comparing to regular .string(): it has password format and generated RegExp string for pattern.
// Expect user input to be a "string" type
z.password()
// Expect password length to be greater or equal to 8
z.password().min(8)
// Expect password length to be less or equal to 100
z.password().max(100)
// Expect password to have at least one digit
z.password().atLeastOne('digit')
// Expect password to have at least one lowercase letter
z.password().atLeastOne('lowercase')
// Expect password to have at least one uppercase letter
z.password().atLeastOne('uppercase')
// Expect password to have at least one special symbol
z.password().atLeastOne('special')Errors:
invalid_password_no_digitinvalid_password_no_lowercaseinvalid_password_no_uppercaseinvalid_password_no_specialtoo_smallwithtype === 'password'too_bigwithtype === 'password'
Caution
@nest-zod/z is deprecated and will not be supported soon. It is recommended to use zod directly. See MIGRATION.md for more information.
Created for
nestjs-zod-prisma
z.json()Caution
@nest-zod/z is deprecated and will not be supported soon. It is recommended to use zod directly. See MIGRATION.md for more information.
Created for custom schemas in
nestjs-zod-prisma
Just returns the same Schema
z.from(MySchema)Caution
@nest-zod/z is deprecated and will not be supported soon. It is recommended to use zod directly. See MIGRATION.md for more information.
Currently, we use custom error code due to some Zod limitations (errorMap priorities)
Therefore, the error details is located inside params property:
const error = {
code: 'custom',
message: 'Invalid date, expected it to be the past',
params: {
isNestJsZod: true,
code: 'invalid_date_string_direction',
// payload is always located here in a flat view
expected: 'past',
},
path: ['date'],
}Caution
@nest-zod/z/frontend is deprecated and will not be supported soon. It is recommended to use zod directly. See MIGRATION.md for more information.
Optionally, you can install @nest-zod/z on the client side.
The library provides you a @nest-zod/z/frontend entry point, that can be used to detect custom NestJS Zod issues and process them the way you want.
import { isNestJsZodIssue, NestJsZodIssue, ZodIssue } from '@nest-zod/z/frontend'
function mapToFormErrors(issues: ZodIssue[]) {
for (const issue of issues) {
if (isNestJsZodIssue(issue)) {
// issue is NestJsZodIssue
}
}
}
⚠️ If you usezodin your client-side application, and you want to install@nest-zod/ztoo, it may be better to completely switch to@nest-zod/zto prevent issues caused by mismatch betweenzodversions.@nest-zod/z/frontenddoesn't usezodat the runtime, but it uses its types.
Prerequisites:
@nestjs/swaggerwith version^5.0.0installed
Apply the patch patchNestJsSwagger() in your main.ts file before setting up your swagger module:
import { patchNestJsSwagger } from '@at7211/nestjs-zod'
patchNestJsSwagger()For addtional documentation, follow the Nest.js' Swagger Module Guide, or you can see the example application guide here .
Use .describe() method to add Swagger description:
import { z } from 'zod'
const CredentialsSchema = z.object({
username: z.string().describe('This is an username'),
password: z.string().describe('This is a password'),
})You can convert any Zod schema to an OpenAPI JSON object:
import { zodToOpenAPI } from '@at7211/nestjs-zod'
import { z } from 'zod'
const SignUpSchema = z.object({
username: z.string().min(8).max(20),
password: z.string().min(8).max(20),
sex: z
.enum(['male', 'female', 'nonbinary'])
.describe('We respect your gender choice'),
social: z.record(z.string().url())
})
const openapi = zodToOpenAPI(SignUpSchema)The output will be the following:
{
"type": "object",
"properties": {
"username": {
"type": "string",
"minLength": 8,
"maxLength": 20
},
"password": {
"type": "string",
"minLength": 8,
"maxLength": 20
},
"sex": {
"description": "We respect your gender choice",
"type": "string",
"enum": ["male", "female", "nonbinary"]
},
"social": {
"type": "object",
"additionalProperties": {
"type": "string",
"format": "uri"
}
},
"birthDate": {
"type": "string",
"format": "date-time"
}
},
"required": ["username", "password", "sex", "social", "birthDate"]
}-
zod-dto
nestjs-zodincludes a lot of refactored code fromzod-dto. -
zod-nestjs and zod-openapi
These libraries bring some new features compared tozod-dto.
nestjs-zodhas used them too.