11

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!

Haralan Dobrev
  • 7,617
  • 2
  • 48
  • 66
BSmith
  • 163
  • 1
  • 1
  • 7
  • why not stick with defined names in dto? – AZ_ Dec 29 '20 at 15:49
  • Hello AZ_, I don't fully understand your comment. Could you elaborate? – BSmith Dec 30 '20 at 16:21
  • I mean let the prop names be as they are in legacy consumer and use the same or add other getter methods `get lowercase() {return this.camelCase}`. Or maybe add a serializer to exclude the CamelCased values if required. or a `classToClass` serializer with `@Expose({name: lowercase})` – AZ_ Dec 31 '20 at 15:23

3 Answers3

11

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

Jay McDoniel
  • 57,339
  • 7
  • 135
  • 147
  • Hey Jay thanks for the response! What's odd is the objects for the Transform are not available (the value or obj arguments) if the property on the inbound JSON payload is different from that of the DTO. The DTO property is "propertyOne" and inbound JSON payload property is "propertyone". If I change the inbound JSON payload property to "propertyOne" then the Transform has proper value and obj arguments, but that defeats the purpose – BSmith Dec 30 '20 at 16:10
  • @BSmith you're right, I was under the assumption that in `@Transform()` class-transformer would give you access to the full object, regardless of the property. I've updated my answer to provide something that _does_ work, but it's not necessarily pretty. – Jay McDoniel Dec 30 '20 at 18:38
  • No need of getters to rename when using `classToPlain`, `plaintoClass`. rather `@Exclude({ toPlainOnly: true })` use `Expose({name: 'lowercase'})` on any other prop names. – AZ_ Jan 01 '21 at 07:10
  • Thanks. @Expose on the getter method work for me. – Virak Aug 18 '23 at 06:17
11

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> {
  // ...
}
Haralan Dobrev
  • 7,617
  • 2
  • 48
  • 66
eol
  • 23,236
  • 5
  • 46
  • 64
  • 2
    This is another good approach. The thing to be aware of with this is that you _must_ run the `RequestConverterPipe` before the `ValidationPipe` which means that the `ValidationPipe` cannot be globally set. – Jay McDoniel Dec 29 '20 at 14:46
  • Hey Eol thanks for the response! I may have to create a custom PipeTransform class like you illustrated. Even though I'm very attracted to the elegance of a single-line Transform in a DTO like in Jay's example below, the issue I have is when the case of the inbound property doesn't match the case of the DTO's property, I'm not able to get the object and value arguments into the Transform. I just cannot figure out what layer is causing this problem. – BSmith Dec 30 '20 at 16:17
  • 2
    I marked your response as the answer because it seemed like the most simple path forward. While the Exclude and Expose decorators on the DTO would work, I felt I was adding additional weight to the DTO and wanted to keep as much logic out of the DTO as possible. I wanted to maintain clearly separated lines in the architecture, leaving DTOs as lean as possible to fulfill their role as transfer entities, not performing transforms, and leaving transform operations in a class which implements PipeTransform. – BSmith Jan 04 '21 at 20:53
  • Happy it helped! :) – eol Jan 05 '21 at 08:48
0

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) {
        //...
}
noways.
  • 1
  • 1
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Apr 23 '23 at 12:22