Javascript is required
nestjs-zod 4.x[翻译]

✨ 为您的 NestJS 应用提供一个无缝的数据验证解决方案 ✨

https://www.npmjs.com/package/nestjs-zod

核心库功能

  • createZodDto - 从 Zod schemas 创建 DTO 类
  • ZodValidationPipe - 使用 Zod DTOs 验证 ==body / query / params==
  • ZodGuard - 通过验证 ==body / query / params== 来守卫路由

(当您想在其他守卫之前进行验证时很有用)

  • UseZodGuard - @UseGuards(new ZodGuard(source, schema)) 的简写
  • ZodValidationException - 扩展了 Zod 错误的 BadRequestException
  • zodToOpenAPI - 从 Zod schemas 创建 OpenAPI 声明
  • OpenAPI 支持
    • 通过 patch 集成 @nestjs/swagger
    • zodToOpenAPI - 生成高精度的 Swagger Schema
    • Zod DTOs 可以在任何 @nestjs/swagger 装饰器中使用
  • 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。它通过使用我们的自定义 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'
  • invalid_date_string_direction - 不是过去/将来时间
    • Payload:
      • expected -
        'past' | 'future'
        
  • invalid_date_string_day - 不是工作日/周末
    • Payload:
      • expected - 'weekDay' | 'weekend'
  • 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 类型
    }
  }
}

迁移

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 也使用了它们。