Search code examples
validationnestjsdtoclass-validatorclass-transformer

NestJS transform a property using ValidationPipe before validation execution during DTO creation


I'm using the built in NestJS ValidationPipe along with class-validator and class-transformer to validate and sanitize inbound JSON body payloads. One scenario I'm facing is a mixture of upper and lower case property names in the inbound JSON objects. I'd like to rectify and map these properties to standard camel-cased models in our new TypeScript NestJS API so that I don't couple mismatched patterns in a legacy system to our new API and new standards, essentially using the @Transform in the DTOs as an isolation mechanism for the rest of the application. For example, properties on the inbound JSON object:

"propertyone",
"PROPERTYTWO",
"PropertyThree"

should map to

"propertyOne",
"propertyTwo",
"propertyThree"

I'd like to use @Transform to accomplish this, but I don't think my approach is correct. I'm wondering if I need to write a custom ValidationPipe. Here is my current approach.

Controller:

import { Body, Controller, Post, UsePipes, ValidationPipe } from '@nestjs/common';
import { TestMeRequestDto } from './testmerequest.dto';

@Controller('test')
export class TestController {
  constructor() {}

  @Post()
  @UsePipes(new ValidationPipe({ transform: true }))
  async get(@Body() testMeRequestDto: TestMeRequestDto): Promise<TestMeResponseDto> {
    const response = do something useful here... ;
    return response;
  }
}

TestMeModel:

import { IsNotEmpty } from 'class-validator';

export class TestMeModel {
  @IsNotEmpty()
  someTestProperty!: string;
}

TestMeRequestDto:

import { IsNotEmpty, ValidateNested } from 'class-validator';
import { Transform, Type } from 'class-transformer';
import { TestMeModel } from './testme.model';

export class TestMeRequestDto {
  @IsNotEmpty()
  @Transform((propertyone) => propertyone.valueOf())
  propertyOne!: string;

  @IsNotEmpty()
  @Transform((PROPERTYTWO) => PROPERTYTWO.valueOf())
  propertyTwo!: string;

  @IsNotEmpty()
  @Transform((PropertyThree) => PropertyThree.valueOf())
  propertyThree!: string;

  @ValidateNested({ each: true })
  @Type(() => TestMeModel)
  simpleModel!: TestMeModel

}

Sample payload used to POST to the controller:

{
  "propertyone": "test1",
  "PROPERTYTWO": "test2",
  "PropertyThree": "test3",
  "simpleModel": { "sometestproperty": "test4" }
}

The issues I'm having:

  1. The transforms seem to have no effect. Class validator tells me that each of those properties cannot be empty. If for example I change "propertyone" to "propertyOne" then the class validator validation is fine for that property, e.g. it sees the value. The same for the other two properties. If I camelcase them, then class validator is happy. Is this a symptom of the transform not running before the validation occurs?
  2. This one is very weird. When I debug and evaluate the TestMeRequestDto object, I can see that the simpleModel property contains an object containing a property name "sometestproperty", even though the Class definition for TestMeModel has a camelcase "someTestProperty". Why doesn't the @Type(() => TestMeModel) respect the proper casing of that property name? The value of "test4" is present in this property, so it knows how to understand that value and assign it.
  3. Very weird still, the @IsNotEmpty() validation for the "someTestProperty" property on the TestMeModel is not failing, e.g. it sees the "test4" value and is satisfied, even though the inbound property name in the sample JSON payload is "sometestproperty", which is all lower case.

Any insight and direction from the community would be greatly appreciated. Thanks!


Solution

  • As an alternative to Jay's execellent answer, you could also create a custom pipe where you keep the logic for mapping/transforming the request payload to your desired DTO. It can be as simple as this:

    export class RequestConverterPipe implements PipeTransform{
      transform(body: any, metadata: ArgumentMetadata): TestMeRequestDto {
        const result = new TestMeRequestDto();
        // can of course contain more sophisticated mapping logic
        result.propertyOne = body.propertyone;
        result.propertyTwo = body.PROPERTYTWO;
        result.propertyThree = body.PropertyThree;
        return result;
      }
    
    export class TestMeRequestDto {
      @IsNotEmpty()
      propertyOne: string;
      @IsNotEmpty()
      propertyTwo: string;
      @IsNotEmpty()
      propertyThree: string;
    }
    

    You can then use it like this in your controller (but you need to make sure that the order is correct, i.e. the RequestConverterPipe must run before the ValidationPipe which also means that the ValidationPipe cannot be globally set):

    @UsePipes(new RequestConverterPipe(), new ValidationPipe())
    async post(@Body() requestDto: TestMeRequestDto): Promise<TestMeResponseDto> {
      // ...
    }