✨ A seamless validation solution for your NestJS application ✨
by @benlorantfy
✨ Create nestjs DTOs from zod schemas
✨ Validate / parse request body, query params, and url params using zod
✨ Serialize response bodies using zod
✨ Automatically generate OpenAPI documentation using zod
-
Install the package:
npm install nestjs-zod # Note: zod ^3.25.0 || ^4.0.0 is also required
-
Add
ZodValidationPipe
to theAppModule
Show me how
ZodValidationPipe
is required in order to validate the request body, query, and params+ import { APP_PIPE } from '@nestjs/core'; + import { ZodValidationPipe } from 'nestjs-zod'; @Module({ imports: [], controllers: [AppController], providers: [ + { + provide: APP_PIPE, + useClass: ZodValidationPipe, + }, ] }) export class AppModule {}
-
Add
ZodSerializerInterceptor
to theAppModule
Show me how
ZodSerializerInterceptor
is required in order to validate the response bodies- import { APP_PIPE } from '@nestjs/core'; + import { APP_PIPE, APP_INTERCEPTOR } from '@nestjs/core'; - import { ZodValidationPipe } from 'nestjs-zod'; + import { ZodValidationPipe, ZodSerializerInterceptor } from 'nestjs-zod'; @Module({ imports: [], controllers: [AppController], providers: [ { provide: APP_PIPE, useClass: ZodValidationPipe, }, + { + provide: APP_INTERCEPTOR, + useClass: ZodSerializerInterceptor, + }, ] }) export class AppModule {}
-
[OPTIONAL] Add an
HttpExceptionFilter
Show me how
An
HttpExceptionFilter
is required in order to add custom handling for zod errors- import { APP_PIPE, APP_INTERCEPTOR } from '@nestjs/core'; + import { APP_PIPE, APP_INTERCEPTOR, APP_FILTER } from '@nestjs/core'; import { ZodValidationPipe, ZodSerializerInterceptor } from 'nestjs-zod'; + import { HttpExceptionFilter } from './http-exception.filter'; @Module({ imports: [], controllers: [AppController], providers: [ { provide: APP_PIPE, useClass: ZodValidationPipe, }, { provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor, }, { provide: APP_FILTER, useClass: HttpExceptionFilter, } ] }) export class AppModule {} + // http-exception.filter + @Catch(HttpException) + export class HttpExceptionFilter extends BaseExceptionFilter { + private readonly logger = new Logger(HttpExceptionFilter.name); + + catch(exception: HttpException, host: ArgumentsHost) { + if (exception instanceof ZodSerializationException) { + const zodError = exception.getZodError(); + if (zodError instanceof ZodError) { + this.logger.error(`ZodSerializationException: ${zodError.message}`); + } + } + + super.catch(exception, host); + } + }
-
[OPTIONAL] Add
cleanupOpenApiDoc
Important: This step is important if using
@nestjs/swagger
Show me how
cleanupOpenApiDoc
is required if using@nestjs/swagger
to properly post-process the OpenAPI doc- SwaggerModule.setup('api', app, openApiDoc); + SwaggerModule.setup('api', app, cleanupOpenApiDoc(openApiDoc));
Check out the example app for a full example of how to integrate nestjs-zod
in your nestjs application
- Request Validation
- Response Validation
- OpenAPI (Swagger) support
validate
(⚠️ DEPRECATED)ZodGuard
(⚠️ DEPRECATED)@nest-zod/z
(⚠️ DEPRECATED)
function createZodDto<TSchema extends UnknownSchema>(schema: TSchema): ZodDto<TSchema>;
Creates a nestjs DTO from a zod schema. These zod DTOs can be used in place of class-validator
/ class-transformer
DTOs. Zod DTOs are responsible for three things:
- Providing a schema for
ZodValidationPipe
to validate incoming client data against - Providing a compile-time typescript type from the Zod schema
- Providing an OpenAPI schema when using
nestjs/swagger
Note
For this feature to work, please ensure ZodValidationPipe
is setup correctly
schema
- A zod schema. You can "bring your own zod", including zod v3 schemas, v4 schemas, zod mini schemas, etc. The only requirement is that the schema has a method calledparse
import { createZodDto } from '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) {}
@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) {}
}
ZodValidationPipe
is needed to ensure zod DTOs actually validate incoming request data when using @Body()
, @Params()
, or @Query()
parameter decorators
When the data is invalid it throws a ZodValidationException.
import { ZodValidationPipe } from 'nestjs-zod'
import { APP_PIPE } from '@nestjs/core'
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ZodValidationPipe,
},
],
})
export class AppModule {}
import { ZodValidationPipe } from 'nestjs-zod'
// controller-level
@UsePipes(ZodValidationPipe)
class AuthController {}
class AuthController {
// route-level
@UsePipes(ZodValidationPipe)
async signIn() {}
}
export function createZodValidationPipe({ createValidationException }: ZodValidationPipeOptions = {}): ZodValidationPipeClass
Creates a custom zod validation pipe
import { createZodValidationPipe } from 'nestjs-zod'
const MyZodValidationPipe = createZodValidationPipe({
// provide custom validation exception factory
createValidationException: (error: ZodError) =>
new BadRequestException('Ooops'),
})
params.createValidationException
- A callback that will be called with the zod error when a parsing error occurs. Should return a new instance ofError
If the zod request parsing fails, then nestjs-zod
will throw a ZodValidationException
, which will result in the following HTTP response:
{
"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"]
}
]
}
You can customize the exception and HTTP response by either 1)
creating a custom validation pipe using createZodValidationPipe
or 2)
handling ZodValidationException
inside an exception filter
Here is an example exception filter:
@Catch(ZodValidationException)
export class ZodValidationExceptionFilter implements ExceptionFilter {
catch(exception: ZodValidationException) {
exception.getZodError() // -> ZodError
}
}
function ZodSerializerDto(dto: ZodDto<UnknownSchema> | UnknownSchema | [ZodDto<UnknownSchema>] | [UnknownSchema])
Parses / serializes the return value of a controller method using the provided zod schema. This is especially useful to prevent accidental data leaks.
Note
Instead of ZodSerializerDto
, consider using ZodResponse
, which has some improvements over ZodSerializerDto
Note
For this feature to work, please ensure ZodSerializerInterceptor
is setup correctly
options.dto
- A ZodDto (or zod schema) to serialize the response with. If passed with array syntax ([MyDto]
) then it will parse as an array. Note that the array syntax does not work withzod/mini
, because it requires the schema have an.array()
method
const UserSchema = z.object({ username: string() })
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)
}
}
In the above example, if the userService.findOne
method returns password
, the password
property will be stripped out thanks to the @ZodSerializerDto
decorator.
Also note that arrays can be serialized using []
syntax like this:
class BookDto extends createZodDto(z.object({ title: string() })) {}
@Controller('books')
export class BooksController {
constructor() {}
@ZodSerializerDto([BookDto])
getBooks() {
return [{ title: 'The Martian' }, { title: 'Hail Marry' }];
}
}
Or by using an array DTO:
class BookListDto extends createZodDto(z.array(z.object({ title: string() }))) {}
@Controller('books')
export class BooksController {
constructor() {}
@ZodSerializerDto(BookListDto)
getBooks() {
return [{ title: 'The Martian' }, { title: 'Hail Marry' }];
}
}
To ensure ZodSerializerDto
works correctly, ZodSerializerInterceptor
needs to be added to the AppModule
Note
Also see ZodSerializationException
for information about customizing the serialization error handling
This should be done in the AppModule
like so:
@Module({
...
providers: [
...,
{ provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor },
],
})
export class AppModule {}
function ZodResponse<TSchema extends UnknownSchema>({ status, description, type }: { status?: number, description?: string, type: ZodDto<TSchema> & { io: "input" } }): (target: object, propertyKey?: string | symbol, descriptor?: Pick<TypedPropertyDescriptor<(...args: any[]) => input<TSchema>|Promise<input<TSchema>>>, 'value'>) => void
function ZodResponse<TSchema extends RequiredBy<UnknownSchema, 'array'>>({ status, description, type }: { status?: number, description?: string, type: [ZodDto<TSchema> & { io: "input" }] }): (target: object, propertyKey?: string | symbol, descriptor?: Pick<TypedPropertyDescriptor<(...args: any[]) => Array<input<TSchema>>|Promise<Array<input<TSchema>>>>, 'value'>) => void
Consolidation of multiple decorators that allows setting the run-time, compile-time, and docs-time schema all at once
Note
For this feature to work, please ensure ZodSerializerInterceptor
and cleanupOpenApiDoc
are setup correctly
params.status
- Optionally sets the "happy-path"status
of the response. If provided, sets the status code using@HttpCode
fromnestjs/common
and using@ApiResponse
fromnestjs/swagger
params.description
- Optionally sets a description of the response using@ApiResponse
params.type
- Sets the run-time (via@ZodSerializerDto
), compile-time (via TypeScript), and docs-time (via@ApiResponse
) response type.
You may find yourself duplicating type information:
@ZodSerializer(BookDto)
@ApiOkResponse({
status: 200,
type: BookDto
})
getBook(): BookDto {
...
}
Here, BookDto
is repeated 3 times:
- To set the DTO to use to serialize
- To set the DTO to use for the OpenAPI documentation
- To set the return type for the function
If these 3 spots get out of sync, this may cause bugs. If you want to remove this duplication, you can consolidate using ZodResponse
:
- @ZodSerializer(BookDto)
- @ApiOkResponse({
- status: 200,
- type: BookDto.Output
- })
- getBook(): BookDto {
+ @ZodResponse({ type: BookDto })
+ getBook()
...
}
@ZodResponse
will set all these things. It will set the DTO to use to serialize, it will set the DTO to use for the OpenAPI documentation, and it will throw a compile-time typescript error if the method does not return data that matches the zod input schema
This is pretty powerful, because it ensures the run-time, compile-time, and docs-time representations of your response are all in sync. For this reason, it's recommended to use @ZodResponse
instead of repeating the DTO three times.
If the zod response serialization fails, then nestjs-zod
will throw a ZodSerializationException
, which will result in the following HTTP response:
{
"message": "Internal Server Error",
"statusCode": 500,
}
You can customize the exception and HTTP response handling ZodSerializationException
inside an exception filter
See the example app here for more information.
Note
For additional documentation, follow Nest.js' Swagger Module Guide, or you can see the example application here
If you have @nestjs/swagger
setup, documentation will automatically be generated for:
- Request bodies, if you use
@Body() body: MyDto
- Response bodies, if you use
@ApiOkResponse({ type: MyDto.Output })
(or@ZodResponse({ type: MyDto })
) - Query params, if you use
@Query() query: MyQueryParamsDto
To generate the OpenAPI document, nestjs-zod
uses z.toJSONSchema
for zod v4 schemas. It's recommended to review the zod documentation itself for more information about how the OpenAPI document is generated
For zod v3 schemas, nestjs-zod
uses a custom-built (deprecated) function called zodV3ToOpenAPI
that generates the OpenAPI document by inspecting the zod schema directly.
However, please ensure cleanupOpenApiDoc
is setup correctly as detailed below
function cleanupOpenApiDoc(doc: OpenAPIObject, options?: { version?: '3.1' | '3.0' | 'auto' }): OpenAPIObject
Cleans up the generated OpenAPI doc by applying some post-processing
Note
There used to be a function called patchNestJsSwagger
. This function has been replaced by cleanupOpenApiDoc
doc
- The OpenAPI doc generated bySwaggerModule.createDocument
options.version
- The OpenAPI version to use while cleaning up the document.auto
(default) - Uses the version specified in the OpenAPI document (The version in the OpenAPI can be changed by using thesetOpenAPIVersion
method on the swagger document builder).3.1
- Nullable fields will useanyOf
and{ type: 'null' }
3.0
- Nullable fields will usenullable: true
To complete the swagger integration/setup, cleanupOpenApiDoc
needs to be called with the generated open api doc, like so:
const openApiDoc = SwaggerModule.createDocument(app,
new DocumentBuilder()
.setTitle('Example API')
.setDescription('Example API description')
.setVersion('1.0')
.build(),
);
- SwaggerModule.setup('api', app, openApiDoc);
+ SwaggerModule.setup('api', app, cleanupOpenApiDoc(openApiDoc));
Note that z.toJSONSchema
can generate two versions of any zod schema: "input" or "output". This is what the zod documentation says about this:
Some schema types have different input and output types, e.g. ZodPipe, ZodDefault, and coerced primitives.
Note that by default, when generating OpenAPI documentation, nestjs-zod
uses the "input" version of a schema, except for @ZodResponse
which always generates the "output" version of a schema. If you want to explicitly use the "output" version of a schema when generating OpenAPI documentation, you can use the .Output
property of a zod DTO. For example, this makes sense when using @ApiResponse
:
@ApiResponse({
type: MyDto.Output
})
However, it's recommended to use @ZodResponse
over @ApiResponse
, which automatically handles this for you:
@ZodResponse({
type: MyDto // <-- No need to do `.Output` here
})
You can also externalize and reuse schemas across multiple DTOs. If you add .meta({ id: "MySchema" })
to any zod schema, then that schema will be added directly to components.schemas
in the OpenAPI documentation. For example, this code:
const Author = z.object({ name: z.string() }).meta({ id: "Author" })
class BookDto extends createZodDto(z.object({ title: z.string(), author: Author })) { }
class BlogPostDto extends createZodDto(z.object({ title: z.string(), author: Author })) { }
Will result in this OpenAPI document:
Caution
zodV3ToOpenAPI
is deprecated and will not be supported soon, since zod v4 adds built-in support for generating OpenAPI schemas from zod schemas. See MIGRATION.md for more information.
Show documentation for deprecated APIs
You can convert any Zod schema to an OpenAPI JSON object:
import { zodToOpenAPI } from '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 = zodV3ToOpenAPI(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"]
}
Caution
validate
is deprecated and will not be supported soon. It is recommended to use .parse
directly. See MIGRATION.md for more information.
Show documentation for deprecated APIs
If you don't like ZodGuard
and ZodValidationPipe
, you can use validate
function:
import { validate } from 'nestjs-zod'
validate(wrongThing, UserDto, (zodError) => new MyException(zodError)) // throws MyException
const validatedUser = validate(
user,
UserDto,
(zodError) => new MyException(zodError)
) // returns typed value when succeed
Caution
Guard-related functions are deprecated and will not be supported soon. It is recommended to use guards for authorization, not validation. See MIGRATION.md for more information.
Show documentation for deprecated APIs
[!CAUTION]
ZodGuard
is deprecated and will not be supported soon. It is recommended to use guards for authorization, not validation. See MIGRATION.md for more information.
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 'nestjs-zod'
// controller-level
@UseZodGuard('body', CredentialsSchema)
@UseZodGuard('params', CredentialsDto)
class MyController {}
class MyController {
// route-level
@UseZodGuard('query', CredentialsSchema)
@UseZodGuard('body', CredentialsDto)
async signIn() {}
}
[!CAUTION]
createZodGuard
is deprecated and will not be supported soon. It is recommended to use guards for authorization, not validation. See MIGRATION.md for more information.
import { createZodGuard } from 'nestjs-zod'
const MyZodGuard = createZodGuard({
// provide custom validation exception factory
createValidationException: (error: ZodError) =>
new BadRequestException('Ooops'),
})
Caution
@nest-zod/z
is no longer supported and has no impact on the OpenAPI generation. It is recommended to use zod
directly. See MIGRATION.md for more information.
Show documentation for deprecated package
@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 no longer supported and has no impact on the OpenAPI generation. It is recommended to usezod
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:33Z
2022-05-02:08:33.000Z
2022-05-02:08:33+00:00
2022-05-02:08:33-00:00
2022-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_small
withtype === 'date_string_year'
-
too_big
withtype === 'date_string_year'
[!CAUTION]
@nest-zod/z
is no longer supported and has no impact on the OpenAPI generation. It is recommended to usezod
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_digit
invalid_password_no_lowercase
invalid_password_no_uppercase
invalid_password_no_special
too_small
withtype === 'password'
too_big
withtype === 'password'
This library was originally created by risen228 and now maintained by BenLorantfy