class-transformer icon indicating copy to clipboard operation
class-transformer copied to clipboard

fix: @Transform() is not called for undefined values if `exposeDefaultValues=true`

Open keyCat opened this issue 2 years ago • 4 comments

Description

@Transform() is not being called for properties with undefined values if exposeDefaultValues set to true. While this may be intentional behaviour, I do not think that this is correct, since not every property of a class can define a default value.

Minimal code-snippet showcasing the problem

export class Example {
  propertyA: string;
  
  @Transform(({ value }) => {
    console.log('propertyB is:', value);
    return value; 
   })
  propertyB?: string;
}

plainToInstance(Example, {  propertyA: 'value' });
// > propertyB is: undefined

plainToInstance(Example, {  propertyA: 'value' }, { exposeDefaultValues: true });
// no console.log

Expected behavior

In my opinion, transformation must be called always in this case.

Actual behavior

See Description

keyCat avatar Sep 04 '22 16:09 keyCat

the same ploblem

pereslavtsev avatar Sep 15 '22 12:09 pereslavtsev

same problem here

Viktor-Bredihin avatar Feb 26 '23 05:02 Viktor-Bredihin

In order to get the Transform callback to be called you should also add the @Expose decorator above the desired property.

import { Expose, Transform, plainToInstance } from 'class-transformer';

class WithoutExpose {
  @Transform(({ value }) => {
    return value ?? -1;
  })
  randomNumber: number;
}

class WithExpose {
  @Expose()
  @Transform(({ value }) => {
    return value ?? -1;
  })
  randomNumber: number;
}

const withoutExpose = plainToInstance(WithoutExpose, {});
console.log(withoutExpose); // -> WithoutExpose {}

const withExpose = plainToInstance(WithExpose, {});
console.log(withExpose); // -> WithExpose { randomNumber: -1 }

nino-vrijman avatar Sep 05 '23 13:09 nino-vrijman

@nino-vrijman The issue is specifically about behaviour with exposeDefaultValues flag. You do not set it in your example, thus if you expose any properties of the class that have a static default value, these values will be disregarded without exposeDefaultValues: true:

class WithoutExpose {
  @Transform(({ value }) => {
    return value ?? -1;
  })
  randomNumber: number;

  notRandomNumber: number = 3;
}

class WithExpose {
  @Expose()
  @Transform(({ value }) => {
    return value ?? -1;
  })
  randomNumber: number;

  @Expose() notRandomNumber: number = 3;
}

const withoutExpose = plainToInstance(WithoutExpose, {});
console.log(withoutExpose); // -> WithoutExpose { notRandomNumber: 3 }

const withExpose = plainToInstance(WithExpose, {});
console.log(withExpose); // -> WithExpose { notRandomNumber: undefined, randomNumber: -1 }

Now, to compare the same example with exposeDefaultValues: true

class WithoutExpose {
  @Transform(({ value }) => {
    return value ?? -1;
  })
  randomNumber: number;

  notRandomNumber: number = 3;
}

class WithExpose {
  @Expose()
  @Transform(({ value }) => {
    return value ?? -1;
  })
  randomNumber: number;

  @Expose() notRandomNumber: number = 3;
}

const withoutExpose = plainToInstance(WithoutExpose, {}, { exposeDefaultValues: true });
console.log(withoutExpose); // -> WithoutExpose { notRandomNumber: 3 }

const withExpose = plainToInstance(WithExpose, {}, { exposeDefaultValues: true });
console.log(withExpose); // -> WithExpose { notRandomNumber: 3, randomNumber: undefined }

As you can see, when you are using @Expose, you can either have static default values, or transformations for undefined values, but not both at the same time, thus the issue.

What I ended up doing for my projects, is ditching a valid language feature and creating a transformation decorator for static default values (exposeDefaultValues should not be set to use it):

import { Transform } from 'class-transformer';
import { TransformOptions } from 'class-transformer/types/interfaces';

export function DefaultValue(val: any, options?: TransformOptions): PropertyDecorator {
  return Transform(({ value }) => {
    if (value === undefined) return val;
    return value;
  }, options);
}

class Example {
  @Expose()
  @DefaultValue(1)
  page: number;
}

keyCat avatar Nov 19 '23 03:11 keyCat