Skip to content

Instantly share code, notes, and snippets.

@GHkrishna
Last active October 14, 2025 11:11
Show Gist options
  • Select an option

  • Save GHkrishna/3b38872ba8c2eb1d299d0a943013de49 to your computer and use it in GitHub Desktop.

Select an option

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.

@barraponto
Copy link

Turns out inspecting the controller requires the ValidationPipe to be request-scoped which means a new instance every request. I'll yield and use the DTO decorator approach.

@GHkrishna
Copy link
Author

That's cool, @barraponto . So you say this should work as it is intended to be??!!

@barraponto
Copy link

It is prone to issues as I've mentioned before.

UpdatableValidationPipe.transform modifies the pipe instance options with the overrides, runs parent ValidationPipe.transform and then resets the pipe instance options. The issue here is pipes are usually singletons. By modifying the instance, you're modifying it globally since there is only one instance.

Instead, the safer way would be to check for overrides and, if found, to create a new instance of ValidationPipe and use that instance's transform method. I would look something like this:

   if (!overrideOptions) return super.transform(value, metadata);

    const validatorOptions = {
      ...this.defaultValidatorOptions,
      ...overrideOptions,
    };
    const validator = new ValidationPipe(validatorOptions);
    return validator.transform(value, metadata);
  }

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