プログラミング実践

【NestJS】ValidationPipeの「property should not exist」エラーで半日溶かした話

記事内に商品プロモーションを含む場合があります

チーム開発でNestJSのAPIを触り始めたとき、商品情報を登録するエンドポイントを作ってリクエストを送ったらこんなエラーが返ってきました。

{
  "statusCode": 400,
  "message": ["property name should not exist"],
  "error": "Bad Request"
}

いや、リクエストの型を定義するクラスに name: string; ってちゃんと書いたじゃないですか……と思いながら、半日潰しました。
同じところで詰まっている人の参考になればと思って書きます。

結論(先出し)

@Body()で受け取るDTOクラスに、class-validatorのデコレータ(@IsString()@IsNumber()など)をつけていなかったことが原因でした。

NestJSでGlobal ValidationPipeをwhitelist: trueで設定している場合、デコレータのないプロパティは「存在してはいけないもの」として400エラーになります。

解決策は一言でいうと、DTOの全プロパティにclass-validatorのデコレータをつけるだけです。

まず「DTO」と「ValidationPipe」って何?

NestJSを触り始めたばかりの方向けに、簡単に整理します(自分も最初は「DTO?なにそれ?」でした)。

NestJSのリクエスト処理の全体像

フロントエンドからAPIにリクエストが届いてDBに保存されるまで、こんな流れになっています。

NestJSのリクエスト処理フロー(フロントエンド→DTO→ValidationPipe→Controller→Service→DB)

DTOとは?

DTO(Data Transfer Object)は、「APIが受け取るデータの形を定義するクラス」です。

たとえば「商品作成APIには name(文字列)と price(数値)が必要」という設計なら、それをTypeScriptのクラスで表現したものがDTOです。

// これがDTO(リクエストの"型"を定義するクラス)
export class CreateProductRequest {
  name: string;
  price: number;
}

ValidationPipeとは?

ValidationPipeは、リクエストが届いたとき「DTOで定義した形式通りのデータかどうか」を自動でチェックしてくれる仕組みです。

おかしなデータが来たら、Controllerより手前で弾いてくれます。Pythonで自前で書いていた「if request.dataがおかしかったら400を返す」みたいな処理を、フレームワーク側がやってくれるイメージです。

状況の説明

実際に書いていたDTOはこんな感じです。見た目は普通ですよね?

import { ApiProperty } from '@nestjs/swagger';

export class CreateProductRequest {
  @ApiProperty()
  name: string;

  @ApiProperty()
  categorySlug: string;

  @ApiProperty()
  sku: string;
}

見た目はおかしくないですよね。プロパティに@ApiProperty()もついているし、TypeScriptの型も書いてある。
でも、これでリクエストを送るとname should not exist」と怒られます。

しかもローカルではなぜか動いていたので(後述します)、さらに混乱しました。

何が起きていたか

「ホワイトリスト」という仕組み

NestJSプロジェクトのmain.tsに、こんな設定が入っていることがあります。

app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,          // ← ホワイトリスト機能をON
    forbidNonWhitelisted: true, // ← ホワイトリスト外は400エラーに
  }),
);

whitelist: trueをONにすると、ValidationPipeは「class-validatorのデコレータがついているプロパティだけをホワイトリスト(許可リスト)に登録する」という動作になります。

さらにforbidNonWhitelisted: trueがあると、ホワイトリストに載っていないプロパティがリクエストに含まれていたとき、黙って無視するのではなく400エラーを返します。

whitelist: true
“If set to true, validator will strip validated (returned) object of any properties that do not use any validation decorators.”

forbidNonWhitelisted: true
“If set to true, instead of stripping non-whitelisted properties validator will throw an exception.”

NestJS公式 – Validation

図で表すとこんなイメージです:

ValidationPipeがホワイトリストで各プロパティを確認し、デコレータがないと400 Bad Requestになる流れ

つまり@ApiProperty()はAPI仕様書を自動生成するためのデコレータで、データのチェックはしてくれません。自分はここを完全に誤解していました。

ローカルで動いていた理由

ローカル環境ではValidationPipeのグローバル設定が緩い状態になっていました。
AWSのテスト環境でのみ再現するバグだったので、余計に原因の特定に時間がかかりました。

「ローカルで動いてるのに本番系だとダメ」というパターン、Pythonの開発ではあまり経験がなかったんですが、Webバックエンドだと環境差分がこういう形で出るんですね。

解決策

DTOの全プロパティにclass-validatorのデコレータをつけるだけです。

import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsOptional } from 'class-validator';

export class CreateProductRequest {
  @ApiProperty()
  @IsString()        // ← 追加(ホワイトリストに登録される)
  name: string;

  @ApiProperty()
  @IsString()        // ← 追加
  categorySlug: string;

  @ApiProperty()
  @IsString()        // ← 追加
  sku: string;

  @ApiProperty({ required: false })
  @IsOptional()      // ← 任意項目はこれ(省略可能にする)
  @IsString()
  description?: string;
}

これだけで、ValidationPipeがこのプロパティたちを「OK、許可リストに入ってる」と判断してくれます。

型ごとのデコレータの対応はこんな感じです:

TypeScript の型 使うデコレータ 意味
string @IsString() 文字列であることを確認
number(一般) @IsNumber() 数値であることを確認
number(整数) @IsInt() 整数であることを確認
number(正の整数) @IsInt() @IsPositive() 1以上の整数を確認
任意項目 @IsOptional() 省略可能(他のデコレータより前に書く)
enum @IsEnum(YourEnum) Enumの値であることを確認
ネストしたオブジェクト @ValidateNested() @Type() 子クラスも検証する

ネストしたDTOがある場合は子クラスも忘れずに

DTOの中にさらに別のクラスが入っている(ネスト)場合、子クラスにも同様にデコレータをつける必要があります。
自分が詰まったケースでも、親DTOだけ直して子DTOを忘れ、同じエラーが再発しました(笑)。

import { Type } from 'class-transformer';
import { IsArray, IsInt, IsString, Min, ValidateNested } from 'class-validator';

// 子クラスにもデコレータが必要
class LineItem {
  @IsString()
  name: string;

  @IsInt()
  @Min(1)
  quantity: number;
}

export class CreateOrderRequest {
  @IsString()
  orderId: string;

  @IsArray()
  @ValidateNested({ each: true }) // 配列の各要素を検証
  @Type(() => LineItem)           // JSONをLineItemのインスタンスに変換
  items: LineItem[];
}

@Type(() => LineItem)class-transformerのデコレータ)は、受け取ったJSONをLineItemクラスのインスタンスに変換するために必要です。
これがないと@ValidateNested()が子クラスのバリデーションを実行できません。

まとめ・感想

整理すると:

  • DTOの全プロパティ@IsString()などのclass-validatorデコレータが必要
  • @ApiProperty()はAPI仕様書用なので、バリデーションとは別物
  • ネストしたDTOは子クラスにも忘れずデコレータをつける
  • ローカルとAWSのテスト環境で挙動が違う場合、ValidationPipeの設定差を疑う

正直、「型を書いたのになんでエラーになるんだ」という気持ちで最初はかなり混乱しました。
でも仕組みがわかってしまえば「デコレータをつけるだけ」なので、次からは同じミスをしなくなりました。半日、返してくれ〜という感じですが笑

参考になれば幸いです。

ところで最近ぜんぜん更新できていなくてすみません。結婚したりと色々バタバタしていました。
落ち着いてきたのでまたコツコツ書いていきます。引き続きよろしくお願いします。

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA