Skip to content

Instantly share code, notes, and snippets.

@GHkrishna
Last active June 12, 2025 20:50
Show Gist options
  • Save GHkrishna/3b38872ba8c2eb1d299d0a943013de49 to your computer and use it in GitHub Desktop.
Save GHkrishna/3b38872ba8c2eb1d299d0a943013de49 to your computer and use it in GitHub Desktop.
NestJS Per DTO ValidationPipe Override | Customize Validation Options like whitelist, transform, forbidNonWhitelisted Per Class

📘 Description:

A clean workaround for overriding ValidationPipe options (like whitelist, transform, and forbidNonWhitelisted) per DTO in NestJS. Useful when you need dynamic fields in some classes while keeping strict validation globally. Includes custom decorator, extended validation pipe, and usage pattern. Inspired by NestJS GitHub issues and updated for modern NestJS projects.

Tags/keywords:

NestJS validation

NestJS ValidationPipe override

DTO-specific validation NestJS

whitelist true override per class

NestJS dynamic fields in DTO

ValidationPipe whitelist exception

NestJS class-validator per DTO config

UpdatableValidationPipe — Preserve Global Config, Override Per-DTO

Context:

NestJS doesn’t allow updating global ValidationPipe settings once configured. This workaround, inspired by discussions at:

NestJS’s ValidationPipe is powerful but rigid—once applied globally (via useGlobalPipes), its options like whitelist or forbidNonWhitelisted can't be adjusted per DTO. This becomes problematic when you want strict validation globally, but still need to allow flexible, dynamic fields in specific DTOs (e.g., forms or JSON structures with unknown keys). NestJS doesn’t provide a built-in way to override these global options per class—so this workaround enables you to override validation behavior selectively while preserving the original global settings.

This approach lets you:

  1. Use a custom global pipe that supports per-DTO overrides, preserving original settings.
  2. Override validation options only for specific DTOs.

rewrite-validation-options.decorator.ts

import {
  ArgumentMetadata,
  Injectable,
  SetMetadata,
  ValidationPipe,
  ValidationPipeOptions,
} from '@nestjs/common';
import { ValidatorOptions } from 'class-validator';
import { Reflector } from '@nestjs/core';

export const REWRITE_VALIDATION_OPTIONS = 'rewrite_validation_options';

export function RewriteValidationOptions(options: ValidatorOptions) {
  return SetMetadata(REWRITE_VALIDATION_OPTIONS, options);
}

@Injectable()
export class UpdatableValidationPipe extends ValidationPipe {
  private readonly defaultValidatorOptions: ValidatorOptions;

  constructor(
    private reflector: Reflector,
    globalOptions: ValidationPipeOptions = {}
  ) {
    super(globalOptions);
    this.defaultValidatorOptions = {
      whitelist: globalOptions.whitelist,
      forbidNonWhitelisted: globalOptions.forbidNonWhitelisted,
      skipMissingProperties: globalOptions.skipMissingProperties,
      forbidUnknownValues: globalOptions.forbidUnknownValues,
    };
  }

  async transform(value: any, metadata: ArgumentMetadata) {
    const overrideOptions = this.reflector.get<ValidatorOptions>(
      REWRITE_VALIDATION_OPTIONS,
      metadata.metatype
    );

    if (overrideOptions) {
      const original = { ...this.validatorOptions };
      this.validatorOptions = {
        ...this.defaultValidatorOptions,
        ...overrideOptions,
      };

      try {
        const res = await super.transform(value, metadata);
        this.validatorOptions = original;
        return res;
      } catch (err) {
        this.validatorOptions = original;
        throw err;
      }
    }

    return super.transform(value, metadata);
  }
}

Initialize the Global Pipe in main.ts

import { NestFactory } from '@nestjs/core';
import { Reflector } from '@nestjs/core';
import { UpdatableValidationPipe } from './pipes/rewrite-validation-options.decorator';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const reflector = app.get(Reflector);

  app.useGlobalPipes(
    new UpdatableValidationPipe(reflector, {
      whitelist: true,
      transform: true,
      forbidNonWhitelisted: true,
    })
  );

  await app.listen(3000);
}
bootstrap();

Override Validation Options on a Specific DTO

import { RewriteValidationOptions } from './pipes/rewrite-validation-options.decorator';

@RewriteValidationOptions({ whitelist: false })
export class MyDto {
  @ApiProperty()
  myField: string;

  // Unknown fields *won’t* be stripped for this DTO
}

Summary

  • ✅ Global validation settings (e.g. whitelist: true) stay in place.
  • ✅ Per-DTO override (@RewriteValidationOptions) temporarily applies only for that class and restores defaults after validation.

You now have global validation enforcement with fine-grained DTO-level control—perfect solution!

Other approaches:

Also some other references that I found, which can be explored too: https://gist.github.com/josephdpurcell/9af97c36148673de596ecaa7e5eb6a0a However, this seems to be the easiest one.

Below is the comparison of the approach we describe here and the approach the author suggests in the above linked gist.

When to use which approach:

When to Use Each Approach

Use @RewriteValidationOptions + UpdatableValidationPipe when:

  • You want to keep global validation but override it per DTO (e.g., disable whitelist for a dynamic form).
  • You still want class-level validation (e.g., @IsString) to apply.
  • You want clear and reusable code that integrates well with tools like Swagger.
  • You want to avoid side effects or global trade-offs like disabling validateCustomDecorators.

Use @RawBody() or custom param decorators only when:

  • You want to completely bypass global validation on very specific routes.
  • You don’t need DTO-based validation.
  • You're okay not using class-validator for those routes.
  • You are working on quick patches or edge cases (but this is risky for long-term maintainability).

Final thoughts:

The custom UpdatableValidationPipe + @RewriteValidationOptions() seems to be the better approach in most cases.

  • It preserves the global configuration.
  • Allows fine-grained control per DTO.
  • Keeps class-validator decorators active.
  • More robust, scalable, and less error-prone.

The @RawBody() method is clever and can be useful in extremely limited cases, but it's easy to misuse and breaks expected NestJS validation behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment