🐕

[NestJS] ValidationPipeとDTOを使った数字のリクエストパラメータの型変換とバリデーション

2023/10/31に公開1

リクエストのパラメータが文字列として取得される問題

以下のようにControllerとDTOを用意します。

import { Controller, Get, Query } from '@nestjs/common';
import { HelloDto } from './dto/hello-dto';

@Controller()
export class AppController {
  @Get()
  getHello(@Query() helloDto: HelloDto) {
    console.log('helloDto', helloDto);
    return {
      message: 'hello',
    };
  }
}
export class HelloDto {
  page: number;
}

リクエストすると出力は以下のようにpageが文字列となります。

helloDto { page: '1' }

また、以下のようにValidationPipe, class-validatorを使ってバリデーションをすると、pageパラメータに数値を指定しているのにバリデーションエラーが発生してしまう。

import { Controller, Get, Query, ValidationPipe } from '@nestjs/common';
import { HelloDto } from './dto/hello-dto';

@Controller()
export class AppController {
  @Get()
  getHello(@Query(new ValidationPipe()) helloDto: HelloDto) {
    console.log('helloDto', helloDto);
    return {
      message: 'hello',
    };
  }
}
import { IsInt } from 'class-validator';

export class HelloDto {
  @IsInt()
  page: number;
}

リクエストするとバリデーションエラーのレスポンスとなる。

{"message":["page must be an integer number"],"error":"Bad Request","statusCode":400}

これは以下のために起こります。

http://localhost:3000?page=1としてもpage"1"となりstringとなります。
そして、DTOクラスに変換されてもnumberにはならずにstringのままです。
そのためIsInt()に反してエラーになります。

バリデーションエラー解決策

以下の2つが考えられます。

  • 暗黙的に型変換
  • 明示的に型変換

暗黙的に型変換

enableImplicitConversionオプションを使います。

import { Controller, Get, Query, ValidationPipe } from '@nestjs/common';
import { HelloDto } from './dto/hello-dto';

@Controller()
export class AppController {
  @Get()
  getHello(
    @Query(
      new ValidationPipe({
        transformOptions: { enableImplicitConversion: true },
      }),
    )
    helloDto: HelloDto,
  ) {
    console.log('helloDto', helloDto);
    return {
      message: 'hello',
    };
  }
}

これにより、ValidationPipe内以下のthis.transformOptionsenableImplicitConversionオプションが渡され、型変換されます。
https://github.com/nestjs/nest/blob/master/packages/common/pipes/validation.pipe.ts#L124-L128
pageがnumberとなるのでバリデーションが正常に行われるようになります。

明示的に型変換

明示的に変換するには、Transform()デコレータを使います。

import { Transform } from 'class-transformer';
import { IsInt } from 'class-validator';

export class HelloDto {
  @Transform(({ value }) => parseInt(value))
  @IsInt()
  page: number;
}

DTOに上記のように@Transform(({ value }) => parseInt(value))で変換処理をいれておきます。
これにより、以下のentityは型変換されたものになるのでバリデーションが期待通りに動作するようになります。
https://github.com/nestjs/nest/blob/master/packages/common/pipes/validation.pipe.ts#L124-L128

明示的にした方が意図が明確になるので分かりやすいかもしれませんね。

pageの値が数値ではなく文字列として取得される件の解決策

ValidationPipetransformオプションを使用します。

import { Controller, Get, Query, ValidationPipe } from '@nestjs/common';
import { HelloDto } from './dto/hello-dto';

@Controller()
export class AppController {
  @Get()
  getHello(@Query(new ValidationPipe({ transform: true })) helloDto: HelloDto) {
    console.log('helloDto', helloDto);
    return {
      message: 'hello',
    };
  }
}

DTOはTransformしておくこと。

import { Transform } from 'class-transformer';
import { IsInt } from 'class-validator';

export class HelloDto {
  @Transform(({ value }) => parseInt(value))
  @IsInt()
  page: number;
}

これにより、出力は以下となります。

helloDto HelloDto { page: 1 }

transformオプションを使用していない時は、単なるオブジェクトでしたが、オプションを使用するとDTOのインスタンスとなります。
pageも型変換され数値となっています。

これは、ValidationPipeの以下の箇所で、型変換されたDTOのインスタンスが
https://github.com/nestjs/nest/blob/master/packages/common/pipes/validation.pipe.ts#L124-L128
以下で返却されているためです。
https://github.com/nestjs/nest/blob/master/packages/common/pipes/validation.pipe.ts#L151-L153

transformオプションがtrueでないと、this.isTransformEnabledfalseとなり、以下のところでvalue({page: '1'})がそのまま返却されます。
https://github.com/nestjs/nest/blob/master/packages/common/pipes/validation.pipe.ts#L162-L164

最後に

サンプルコードはGitHubにあげています。
https://github.com/hid3h/nestjs-examples/tree/main/validation-pipe-example

この記事が少しでも参考になればいいねを押してもらえれば励みになります!
最後まで読んでいただきありがとうございました。

Discussion

RaisanRaisan

助かりました。ありがとうございます!!