Open14
NestJS で OpenAPI スキーマを Zod スキーマから出力したい(nestjs-zod と @anatine/zod-nestjs の比較)
- NestJSでは、
@nestjs/swagger
の機能を利用して、コードからOpenAPI 定義を生成できる - 基本的にzodスキーマでオブジェクトの型を定義しているので、zodを元に自動生成したい
比較するライブラリ
nestjs-zod
- Weekly Downloads 118,601 (2025-05-31時点)
@anatine/zod-nestjs
- Weekly Downloads 59,603 (2025-05-31時点)
- zodのドキュメントから参照されている https://v3.zod.dev/?id=ecosystem
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)
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ZodValidationPipe,
},
],
})
export class AppModule {}
超便利。
返ってくるエラーはこんな感じ
{
"statusCode": 400,
"message": "Validation failed",
"errors": [
{
"validation": "email",
"code": "invalid_string",
"message": "適切なメールアドレスを入力してください",
"path": [
"userEmail"
]
}
]
}
フィルターでキャッチすることも可能。
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() {
}
nestjs-zod
対応していない。OpenAPIスキーマには空っぽのオブジェクト {}
で生成されてしまう。
PRは出ているが進んでいない様子。
@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)),
});