Open14

NestJS で OpenAPI スキーマを Zod スキーマから出力したい(nestjs-zod と @anatine/zod-nestjs の比較)

かがんかがん
  • NestJSでは、@nestjs/swagger の機能を利用して、コードからOpenAPI 定義を生成できる
  • 基本的にzodスキーマでオブジェクトの型を定義しているので、zodを元に自動生成したい

比較するライブラリ

nestjs-zod

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

  • Weekly Downloads 118,601 (2025-05-31時点)

@anatine/zod-nestjs

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

かがんかがん

nestjs-zod 使ってみる

かがんかがん

リクエストパラメータの定義

省略するけどこんな感じ。
createZodDto によって、DTOクラスに変換してくれる。

import { createZodDto } from 'nestjs-zod';

const updateContentSchema = z.object({
  text: z.string(),
  userEmail: z.string().email("適切なメールアドレスを入力してください"),
})

export class UpdateContentDto extends createZodDto(updateContentSchema) {}
  @Put("update")
  async updateContent(
    @Body() body: UpdateContentDto,
  ) {
    // ...
  }
かがんかがん

パイプを利用することで、バリデーションが行われる。
(グローバルが推奨されているが、個別でもOK)
https://github.com/BenLorantfy/nestjs-zod?tab=readme-ov-file#globally-recommended

@Module({
  providers: [
    {
      provide: APP_PIPE,
      useClass: ZodValidationPipe,
    },
  ],
})
export class AppModule {}

超便利。

返ってくるエラーはこんな感じ

{
  "statusCode": 400,
  "message": "Validation failed",
  "errors": [
    {
      "validation": "email",
      "code": "invalid_string",
      "message": "適切なメールアドレスを入力してください",
      "path": [
        "userEmail"
      ]
    }
  ]
}

フィルターでキャッチすることも可能。
https://github.com/BenLorantfy/nestjs-zod?tab=readme-ov-file#validation-exceptions

かがんかがん

patchNestJsSwagger を入れることで、Swagger (OpenAPI) に反映される。

  patchNestJsSwagger();

  const config = new DocumentBuilder()
    .setTitle('kikagaku-backend-admin API')
    .setVersion('1.0')
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);
かがんかがん

レスポンスデータの定義

同様に、 createZodDto を使ってDTOクラスを作る

const contentSchema = z.object({
  text: z.string(),
  userEmail: z.string().email(),
})

export class ContentDto extends createZodDto(contentSchema) {}
  @ApiOkResponse({ type: ContentDto })
  async getContent() {
  }
かがんかがん

@anatine/zod-nestjs 使ってみる

基本的な使い方は一緒。

かがんかがん
import { createZodDto } from '@anatine/zod-nestjs';

const updateContentSchema = z.object({
  text: z.string(),
  userEmail: z.string().email("適切なメールアドレスを入力してください"),
})

export class UpdateContentDto extends createZodDto(updateContentSchema) {}
  @Put("update")
  async updateContent(
    @Body() body: UpdateContentDto,
  ) {
    // ...
  }
かがんかがん

パイプでバリデーションできる。
ドキュメントに書いてないが、グローバルに適用して大丈夫そう。

@Module({
  providers: [
    {
      provide: APP_PIPE,
      useClass: ZodValidationPipe,
    },
  ],
})
export class AppModule {}

返ってくるエラーの形式が違う、、、
そしてカスタムができないっぽい。これはちょっと苦しい

{
  "message": [
    "updatedUserEmail: 適切なメールアドレスを入力してください"
  ],
  "error": "Bad Request",
  "statusCode": 400
}
かがんかがん

patchNestjsSwagger を入れることで、Swagger (OpenAPI) に反映される。
(nestjs-zodと、Jの大文字・小文字がじつは違う)

import { patchNestjsSwagger } from '@anatine/zod-nestjs';

// ...

  patchNestjsSwagger();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);
かがんかがん

Dateの対応

Dateオブジェクトのデータを返却する際、シリアライズされてISO 8601形式の文字列になる。
これが対応されているかどうか。

const contentSchema = z.object({
  updatedAt: z.date(),
})

export class ContentDto extends createZodDto(contentSchema) {}
  @ApiOkResponse({ type: ContentDto }) // →どうスキーマに反映されるか
  async getContent() {
  }
かがんかがん

@anatine/zod-nestjs

対応してた。{ "type": "string", "format": "date-time" } になってくれた。

かがんかがん

Dateの変換が正しくない問題以外(特にバリデーションエラーの違い)については nestjs-zod を使いたいため、どうにか手元で対応したい。
結果、レスポンスデータの定義のときのみ以下のようにスキーマを変更(既存のスキーマがある場合はextendで上書き)することで対応。

const contentSchema = z.object({
      updatedAt: z
        .string()
        .datetime()
        .transform((data) => new Date(data)),
});

// 既存のがある場合

const contentResponseSchema = content.extend({
      updatedAt: z
        .string()
        .datetime()
        .transform((data) => new Date(data)),
});