✨ 为您的 NestJS 应用提供一个无缝的数据验证解决方案 ✨
nestjs-zod
https://www.npmjs.com/package/nestjs-zod
核心库功能
createZodDto
- 从 Zod schemas 创建 DTO 类ZodValidationPipe
- 使用 Zod DTOs 验证body / query / paramsZodGuard
- 通过验证body / query / params
来守卫路由
(当您想在其他守卫之前进行验证时很有用)
UseZodGuard
-@UseGuards(new ZodGuard(source, schema))
的简写ZodValidationException
- 扩展了 Zod 错误的 BadRequestExceptionzodToOpenAPI
- 从 Zod schemas 创建 OpenAPI 声明- OpenAPI 支持
- 通过 patch 集成
@nestjs/swagger
zodToOpenAPI
- 生成高精度的 Swagger Schema- Zod DTOs 可以在任何
@nestjs/swagger
装饰器中使用
- 通过 patch 集成
- NestJS 的扩展 Zod schemas (
@nest-zod/z
)- 注意:
@nest-zod/z
已经废弃,不久将停止支持
。建议直接使用zod
。详情请参考 MIGRATION.md dateString
用于日期处理(支持转换为Date
类型)password
用于密码(更复杂的字符串规则 + OpenAPI 转换)
- 注意:
- 自定义功能 - 轻松更改异常格式
- 客户端错误处理的实用工具 (
nestjs-zod/frontend
)
安装
npm install nestjs-zod zod
依赖要求:
- zod - >= 3.14.3
- @nestjs/common - >= 8.0.0 (服务端必需)
- @nestjs/core - >= 8.0.0 (服务端必需)
- @nestjs/swagger - >= 5.0.0 (仅在使用 patchNestJsSwagger 时需要)
所有依赖都被标记为可选,以便更好地支持客户端使用。但在服务端使用 nestjs-zod 时,需要安装必需的依赖。
从 Zod schema 创建 DTO
import { createZodDto } from 'nestjs-zod'
import { z } from 'zod'
const CredentialsSchema = z.object({
username: z.string(),
password: z.string(),
})
// 需要类来使用 DTO 作为类型
class CredentialsDto extends createZodDto(CredentialsSchema) {}
使用 DTO
DTO 具有两个功能:
- 为 ZodValidationPipe 提供 schema
- 为您提供来自 Zod schema 的类型
@Controller('auth')
class AuthController {
// 使用全局 ZodValidationPipe (推荐)
async signIn(@Body() credentials: CredentialsDto) {}
async signIn(@Param() signInParams: SignInParamsDto) {}
async signIn(@Query() signInQuery: SignInQueryDto) {}
// 使用路由级别 ZodValidationPipe
@UsePipes(ZodValidationPipe)
async signIn(@Body() credentials: CredentialsDto) {}
}
// 使用控制器级别 ZodValidationPipe
@UsePipes(ZodValidationPipe)
@Controller('auth')
class AuthController {
async signIn(@Body() credentials: CredentialsDto) {}
}
独立使用(不带服务器端依赖)
import { createZodDto } from 'nestjs-zod/dto'
使用 ZodValidationPipe
验证管道使用您的 Zod schema 来解析参数装饰器中的数据。
当数据无效时 - 它会抛出 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'
// 控制器级别
@UsePipes(ZodValidationPipe)
class AuthController {}
class AuthController {
// 路由级别
@UsePipes(ZodValidationPipe)
async signIn() {}
}
您也可以直接传递 Schema 或 DTO:
import { ZodValidationPipe } from 'nestjs-zod'
import { UserDto, UserSchema } from './auth.contracts'
// 使用 schema
@UsePipes(new ZodValidationPipe(UserSchema))
// 使用 DTO
@UsePipes(new ZodValidationPipe(UserDto))
class AuthController {}
创建自定义验证管道
import { createZodValidationPipe } from 'nestjs-zod'
const MyZodValidationPipe = createZodValidationPipe({
// 提供自定义验证异常工厂
createValidationException: (error: ZodError) =>
new BadRequestException('出错了'),
})
使用 ZodGuard
有时,我们需要在特定的 Guards 之前验证用户输入。由于 NestJS 的 Pipes 总是在 Guards 之后执行,所以不能使用 Validation Pipe。
解决方案是使用 ZodGuard
。它的工作方式与 ZodValidationPipe
类似,但不会转换输入数据。
它有两种语法形式:
@UseGuards(new ZodGuard('body', CredentialsSchema))
@UseZodGuard('body', CredentialsSchema)
参数:
- source - '
body
' | 'query
' | 'params
' - Zod Schema 或 DTO (就像
ZodValidationPipe
)
当数据无效时 - 会抛出 ZodValidationException。
import { ZodGuard } from 'nestjs-zod'
// 控制器级别
@UseZodGuard('body', CredentialsSchema)
@UseZodGuard('params', CredentialsDto)
class MyController {}
class MyController {
// 路由级别
@UseZodGuard('query', CredentialsSchema)
@UseZodGuard('body', CredentialsDto)
async signIn() {}
}
创建自定义守卫
import { createZodGuard } from 'nestjs-zod'
const MyZodGuard = createZodGuard({
// 提供自定义验证异常工厂
createValidationException: (error: ZodError) =>
new BadRequestException('出错了'),
})
如果您不喜欢 ZodGuard 和 ZodValidationPipe,您可以使用 validate 函数
import { validate } from 'nestjs-zod'
validate(wrongThing, UserDto, (zodError) => new MyException(zodError)) // 抛出 MyException
const validatedUser = validate(
user,
UserDto,
(zodError) => new MyException(zodError)
) // 成功时返回类型化的值
从头创建验证
如果你不喜欢 ZodGuard 和 ZodValidationPipe,你可以使用 validate 函数:
import { validate } from 'nestjs-zod'
// 验证失败时抛出 MyException
validate(wrongThing, UserDto, (zodError) => new MyException(zodError))
// 验证成功时返回类型化的值
const validatedUser = validate(
user,
UserDto,
(zodError) => new MyException(zodError)
)
异常处理
验证错误时的默认服务器响应格式如下:
{
"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"]
}
]
}
这种结构是由默认的 ZodValidationException 决定的。
你可以通过使用工厂函数创建自定义 nestjs-zod 实体来自定义异常:
- 验证管道
- 守卫
您可以通过提供 ZodError 手动创建 ZodValidationException:
const exception = new ZodValidationException(error)
此外,ZodValidationException 为在 NestJS 异常过滤器中更好地使用提供了额外的 API:
@Catch(ZodValidationException)
export class ZodValidationExceptionFilter implements ExceptionFilter {
catch(exception: ZodValidationException) {
exception.getZodError() // -> 返回 ZodError
}
}
使用 ZodSerializerInterceptor 进行输出验证
为了确保响应符合特定的数据结构,你可以使用 ZodSerializerInterceptor
拦截器。
这在防止意外的数据泄露时特别有用。
这类似于 NestJs 的 @ClassSerializerInterceptor 特性。
在应用根模块中包含 @ZodSerializerInterceptor
@Module({
...
providers: [
...,
{ provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor },
],
})
export class AppModule {}
使用 @ZodSerializerDto 在控制器中定义响应的数据结构
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) {
// 原生服务方法默认返回 { username: string, password: string }
return this.userService.findOne(id)
}
}
在上面的例子中,尽管 userService.findOne 方法会返回 password,但由于 @ZodSerializerDto 装饰器的作用,password 字段会被过滤掉。
OpenAPI (Swagger) 支持
前提条件:
已安装 @nestjs/swagger
版本 ^5.0.0
import { patchNestJsSwagger } from 'nestjs-zod'
patchNestJsSwagger()
然后按照 Nest.js 的 Swagger 模块指南 进行操作。
编写更兼容 Swagger 的 schemas
使用 .describe() 方法添加 Swagger 描述:
import { z } from 'zod'
const CredentialsSchema = z.object({
username: z.string().describe('用户名字段'),
password: z.string().describe('密码字段'),
})
使用 zodToOpenAPI
你可以将任何 Zod schema 转换为 OpenAPI JSON 对象:
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('我们尊重您的性别选择'),
social: z.record(z.string().url())
})
const openapi = zodToOpenAPI(SignUpSchema)
转换输出将如下所示:
{
"type": "object",
"properties": {
"username": {
"type": "string",
"minLength": 8,
"maxLength": 20
},
"password": {
"type": "string",
"minLength": 8,
"maxLength": 20
},
"sex": {
"description": "我们尊重您的性别选择",
"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"]
}
同步到Swagger
您需要将 PatchNestJsSwagger 导入到 main.ts 中并调用它,如下所示:
import { patchNestJsSwagger } from 'nestjs-zod';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
patchNestJsSwagger();
}h
扩展Zod
@nest-zod/z 已弃用,不久将不再支持。建议直接使用 zod。详见 MIGRATION.md。
@nest-zod/z
提供了一个特殊版本的 Zod。它通过使用我们的自定义 schemas 和方法,帮助您更准确地验证用户输入。
ZodDateString 日期字符串处理
在 HTTP 中,我们总是将日期作为字符串接收。但默认的 Zod 只有完整日期时间字符串的验证。ZodDateString
就是为解决这个问题而创建的。
// 1. 期望用户输入为 "string" 类型
// 2. 期望用户输入为有效日期 (通过 new Date 验证)
z.dateString()
// 转换为 Date 实例
// (在链式调用末尾使用,但要在 "describe" 之前)
z.dateString().cast()
// 期望字符串符合 RFC3339 的 "full-date" 格式
z.dateString().format('date')
// [默认格式]
// 期望字符串符合 RFC3339 的 "date-time" 格式
z.dateString().format('date-time')
// 期望日期是过去时间
z.dateString().past()
// 期望日期是将来时间
z.dateString().future()
// 期望年份大于或等于 2000
z.dateString().minYear(2000)
// 期望年份小于或等于 2025
z.dateString().maxYear(2025)
// 期望日期是工作日
z.dateString().weekDay()
// 期望日期是周末
z.dateString().weekend()
合法的 date
格式示例:
- 2022-05-15
合法的 date-time 格式示例:
- 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
错误类型:
invalid_date_string
- 无效日期invalid_date_string_format
- 格式错误- Payload:
- expected - 'date' | 'date-time'
- Payload:
invalid_date_string_direction
- 不是过去/将来时间- Payload:
- expected -
'past' | 'future'
- expected -
- Payload:
- invalid_date_string_day - 不是工作日/周末
- Payload:
- expected - 'weekDay' | 'weekend'
- Payload:
- too_small with
type === 'date_string_year'
- too_big with
type === 'date_string_year'
ZodPassword
ZodPassword
是一个类似字符串的类型,就像 ZodDateString
。
顾名思义,它是为帮助你定义密码验证规则而设计的。 与普通的 .string()
相比,ZodPassword
在 OpenAPI 转换方面更加准确:它具有 `password` 格式和为 `pattern` 生成的 RegExp 字符串。
// 期望用户输入为 "string" 类型
z.password()
// 期望密码长度大于或等于 8
z.password().min(8)
// 期望密码长度小于或等于 100
z.password().max(100)
// 期望密码至少包含一个数字
z.password().atLeastOne('digit')
// 期望密码至少包含一个小写字母
z.password().atLeastOne('lowercase')
// 期望密码至少包含一个大写字母
z.password().atLeastOne('uppercase')
// 期望密码至少包含一个特殊符号
z.password().atLeastOne('special')
错误类型:
- invalid_password_no_digit - 没有数字
- invalid_password_no_lowercase - 没有小写字母
- invalid_password_no_uppercase - 没有大写字母
- invalid_password_no_special - 没有特殊符号
- too_small with type === 'password'
- 密码太短
- too_big with type === 'password'
- 密码太长
Json Schema
Created for nestjs-zod-prisma
z.json()
扩展的 Zod 错误处理
目前,由于 Zod 的一些限制(errorMap 优先级),我们使用 custom 错误代码。
因此,错误详情位于 params 属性中:
const error = {
code: 'custom',
message: '无效的日期,应该是过去时间',
params: {
isNestJsZod: true,
code: 'invalid_date_string_direction',
// payload 总是以扁平形式存在于此
expected: 'past',
},
path: ['date'],
}
客户端错误处理
你可以选择在客户端安装 @nest-zod/z
。
该库提供了一个 @nest-zod/z/frontend
入口点,可用于检测和处理自定义的 NestJS Zod 错误。
import { isNestJsZodIssue, NestJsZodIssue, ZodIssue } from '@nest-zod/z/frontend'
function mapToFormErrors(issues: ZodIssue[]) {
for (const issue of issues) {
if (isNestJsZodIssue(issue)) {
// issue 是 NestJsZodIssue 类型
}
}
}
如果你在客户端应用中使用 zod,并且也想安装 @nest-zod/z,建议完全切换到 @nest-zod/z 以避免 zod 版本不匹配导致的问题。虽然 @nest-zod/z/frontend 在运行时不使用 zod,但它会使用其类型定义。
迁移
nestjs-zod/z is now @nest-zod/z
- import { z } from 'nestjs-zod/z'
+ import { z } from '@nest-zod/z'
致谢
- zod-dto\nnestjs-zod 包含许多来自 zod-dto 的重构代码。
- zod-nestjs and zod-openapi
这些库与 zod-dto 相比带来了一些新功能。 nestjs-zod 也使用了它们。