BSmith
BSmith

Reputation: 223

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!

Upvotes: 14

Views: 57052

Answers (3)

noways.
noways.

Reputation: 11

Here's what I came up with:

export class TransformPipe<T> implements PipeTransform {
    constructor(private rules: Record<any, (value: any) => any> = null) {}

    transform(body: T): T {
        const result: T | null = null;
        for (const key in body) {
            if (this.rules[key])
                result[key] = this.rules[key] ? this.rules[key](body[key]) : body[key];
        }
        return result;
    }
}

And you can use this:

class CoolDto {
    phone: string;
    text: string;
}

@UsePipes(
    new TransformPipe<CoolDto>({
        phone: (v) => v?.trim() || '',
        text: (v) => v?.trim() || '',
    }),
    new ValidationPipe())
    @Post('send')
    async send(@Body() body: CoolDto) {
        //...
}

Upvotes: 1

eol
eol

Reputation: 24565

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> {
  // ...
}

Upvotes: 12

Jay McDoniel
Jay McDoniel

Reputation: 70600

You'll probably need to make use of the Advanced Usage section of the class-transformer docs. Essentially, your @Transform() would need to look something like this:

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

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

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

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

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

}

This should take an incoming payload of

{
  "propertyone": "value1",
  "PROPERTYTWO": "value2",
  "PropertyThree": "value3",
}

and turn it into the DTO you envision.

Edit 12/30/2020

So the original idea I had of using @Transform() doesn't quite work as envisioned, which is a real bummer cause it looks so nice. So what you can do instead isn't quite as DRY, but it still works with class-transformer, which is a win. By making use of @Exclude() and @Expose() you're able to use property accessors as an alias for the weird named property, looking something like this:

class CorrectedDTO {
  @Expose()
  get propertyOne() {
    return this.propertyONE;
  }
  @Expose()
  get propertyTwo(): string {
    return this.PROPERTYTWO;
  }
  @Expose()
  get propertyThree(): string {
    return this.PrOpErTyThReE;
  }
  @Exclude({ toPlainOnly: true })
  propertyONE: string;
  @Exclude({ toPlainOnly: true })
  PROPERTYTWO: string;
  @Exclude({ toPlainOnly: true })
  PrOpErTyThReE: string;
}

Now you're able to access dto.propertyOne and get the expected property, and when you do classToPlain it will strip out the propertyONE and other properties (if you're using Nest's serialization interceptor. Otherwise in a secondary pipe you could plainToClass(NewDTO, classToPlain(value)) where NewDTO has only the corrected fields).

The other thing you may want to look into is an automapper and see if it has better capabilities for something like this.

If you're interested, here's the StackBlitz I was using to test this out

Upvotes: 13

Related Questions