3

I'm struggling to make the fields of my request DTOs case insensitive.

export class ExampleDto {
  dateOfBirth?: string
}

Now I want to accept

  • { "dateofbirth": "19880101" }
  • { "dateOfBirth": "19880101" }
  • { "DATEOFBIRTH": "19880101" }

My first thought was to implement a middleware which just looks at the incoming body and "extends it" with lower & upper case mappings for all incoming fields.

But that doesn't meet my requirements due to camel case, which I definitely want to keep as the default.

Any ideas on how to do this?

tpschmidt
  • 2,479
  • 2
  • 17
  • 30

2 Answers2

0

You could create a custom Pipe where you try the different options and finally return the Dto instance:

export class CaseInsensitiveExampleDtoPipe implements PipeTransform{
  transform(body: any, metadata: ArgumentMetadata): ExampleDto {
    const dto = new ExampleDto();
    dto.dateOfBirth = body.dateOfBirth || body.dateofbirth || body.DATEOFBIRTH;
    return dto;
  }

In your controller you can then use it as follows:

@UsePipes(new CaseInsensitiveExampleDtoPipe())
async postNewExample(@Body() exampleDto: ExampleDto) {
  // ...
}
eol
  • 23,236
  • 5
  • 46
  • 64
  • 2
    Thank you for this suggestion! That would work indeed, but I'm looking for some generic solution that would work for all my DTOs without creating the mappings itself somewhere. I'm currently going with lowercasing all my fields in all DTOs and then just transforming my incoming body in a middleware. – tpschmidt Sep 15 '21 at 13:59
  • 1
    I see - in that case I guess your solution is probably the best :) – eol Sep 15 '21 at 14:38
  • The DTO in question is named "ExampleDto" for a reason... – noamyg Mar 16 '22 at 13:06
  • @eol why would anyone otherwise ask for a middleware to deal with these kinds of conversions? – noamyg Mar 16 '22 at 13:23
  • 1
    Maybe for a special usecase where case-insensitive fields must be supported by an endpoint as I'd usually expect my client to adhere to my API's DTO-models... anyway not gonna argue with you, I was just wondering why it was downvoted, since the "generic" aspect was not mentioned explicitly in the question. – eol Mar 16 '22 at 13:41
0

Since JavaScript properties start existing after their initialization, you cannot "see" the definition of dateOfBirth?: string and therefor you won't be able to match it against the received JSON.

A possible solution for that is to enforce the creation of the properties of all of your DTO's with a constructor:

export class ExampleDto {
    dateOfBirth: string

    constructor(dateOfBirth: string){
        this.dateOfBirth = dateOfBirth;
    }
}

Then, you'll be able to iterate over the ExampleDto's properties and match them with a pipe (the received type can be derived from metadata):

@Injectable()
export class IgnoreCasePipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    const dto = new metadata.metatype;
    const dtoKeys = Object.getOwnPropertyNames(dto);
    Object.keys(value).forEach(key => {
      const realKey = dtoKeys.find(dtoKey => dtoKey.toLocaleLowerCase() === key.toLocaleLowerCase());
      if (realKey) {
        dto[realKey] = value[key];
      }
    });
    return dto;
  }
}

Either inject it globally in main.ts or wherever it's needed - just bear in mind that you'll need to create a constructor for each DTO.

Note: this would work for a single-level class. If you want to support something like people: PersonDto[] in your classes then you'll need to recursively find all of the nested keys and match them - something like this.

noamyg
  • 2,747
  • 1
  • 23
  • 44