#ZodValidationPipe not playing nicely with global ValidationPipe

5 messages · Page 1 of 1 (latest)

red sleet
#

We have a global ValidationPipe set up in main.ts with whitelisting and forbidding of non-whitelisted fields for security purposes, then we have some controller endpoints that we're trying to make use of Zod DTOs with, but when using the createZodDto() function, apparently the created DTO doesn't pass whitelist validation and all the fields I provide show property foobar should not exist.

Am I missing something here? How can we get the application-wide security provided by ValidationPipe while skipping it for generated Zod DTOs? I don't see a way to skip ValidationPipe for certain requests.

idle escarp
#

Sorry if I've got the wrong end of the stick, but it sounds like you just need a Custom Decorator that will skip over the Global Validation Pipe and instead use a Zod Validation Pipe where indicated?

By default the Global Validation Pipe won't run on a Custom Decorator (unless you set validateCustomDecorators: true, but I don't think that's applicable here since you want to validate with Zod directly), instead it only runs on the basic ones (@Body() for example). (see https://docs.nestjs.com/custom-decorators)

So you can just make a Custom Decorator and use it on your Controller with a Zod Validation Pipe like so:

Main.ts:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({whitelist: true, forbidNonWhitelisted: true}))
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();```

app.controller.ts:
```ts
import { Body, Controller, Post } from '@nestjs/common';
import { AppService } from './app.service';
import { CreateAppDto } from './dtos/app-test.dto';
import { ZodValidationPipe } from './pipes/zod-validation.pipe';
import { CreateAppTestZodDto, CreateAppTestZodDtoType } from './dtos/app-test-zod.dto';
import { ZodValidation } from './decorators/zod-validation.decorator';


@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Post()
  createApp(@Body() createDto: CreateAppDto) {
    return this.appService.createTest(createDto)
  }

  @Post('zod')
  createAppWithZod(@ZodValidation(new ZodValidationPipe(CreateAppTestZodDto)) createDto: CreateAppTestZodDtoType
  ) {
    return this.appService.createTest(createDto)
  }
}```
#

zod-validation-decorator.ts:

import { createParamDecorator, ExecutionContext } from "@nestjs/common";

export const ZodValidation = createParamDecorator(
  (data: unknown, context: ExecutionContext) => {
    const request = context.switchToHttp().getRequest()
    return request.body
  }
)```

zod-validation-pipe.ts:
```ts
import { PipeTransform, Injectable, BadRequestException, InternalServerErrorException } from '@nestjs/common';
import { ZodError, ZodType } from 'zod';

@Injectable()
export class ZodValidationPipe implements PipeTransform {
  constructor(private schema: ZodType<any>) {}

  transform(value: any) {
    try {
      const data = this.schema.parse(value);
      return data
    } catch (error) {
      if(error instanceof ZodError){
        throw new BadRequestException(error.issues)
      } else {
        throw new InternalServerErrorException(error.message)
      }
    }
  }
}```
#

app-test.dto:

import { Type } from "class-transformer";
import { IsNotEmpty, IsString } from "class-validator";

export class CreateAppDto {

  @IsNotEmpty()
  @IsString()
  string: string

  @IsNotEmpty()
  @Type(() => Number)
  number: number
}```

app-test-zod.dto:
```ts
import z from "zod";

export const CreateAppTestZodDto = z.object({
  string: z.string().min(1),
  number: z.number(),
  other: z.string().min(3)
})

export type CreateAppTestZodDtoType = z.infer<typeof CreateAppTestZodDto>```
#

app.service.ts: (You'd want to pick which createDto you actually want here to access the relevant params)

import { Injectable } from '@nestjs/common';
import { CreateAppDto } from './dtos/app-test.dto';
import { CreateAppTestZodDtoType } from './dtos/app-test-zod.dto';

@Injectable()
export class AppService {

  createTest(createDto: CreateAppDto | CreateAppTestZodDtoType ) {
    return 'Created!'
  }
}```