If you’ve ever used class-transformer with @Transform, you might have run into this weird situation:
import 'reflect-metadata';
import { plainToInstance, Transform } from 'class-transformer';
import { IsEmail, IsNotEmpty } from 'class-validator';
class MobileSignupBody {
  @IsNotEmpty()
  @IsEmail()
  email!: string;
  @Transform(({ obj }) => obj.email)
  normalized_email?: string;
}
const dto = plainToInstance(MobileSignupBody, { email: 'abc@test.com' });
console.log(dto); // MobileSignupBody { email: 'abc@test.com' }
console.log(dto.normalized_email); // ❌ undefined
Wait… why is normalized_email undefined even though I clearly added a @Transform?
The Gotcha
By default, class-transformer only runs @Transform on properties that exist in the plain object input.
In this case, the plain object only had:
{ "email": "abc@test.com" }
Since there was no normalized_email key, class-transformer skipped that property completely.
That’s why the transform never fired.
  
  
  The Fix: Use @Expose
You need to tell class-transformer: “always include this property, even if it’s not in the plain object.”
import { Expose, Transform } from 'class-transformer';
class MobileSignupBody {
  @Expose() // Will always include in class/plain object -> be transformed
  @IsNotEmpty()
  @IsEmail()
  email!: string;
  @Expose()
  @Transform(({ obj }) => obj.email)
  normalized_email?: string;
}
const dto = plainToInstance(MobileSignupBody, { email: 'abc@test.com' });
console.log(dto.normalized_email); // ✅ "abc@test.com"
Now it works because @Expose forces normalized_email into the transform pipeline.
Global Config Options
If you don’t want to sprinkle @Expose() everywhere, you have a couple of global strategies:
- 
excludeExtraneousValues 
const dto = plainToInstance(MobileSignupBody, { email: 'abc@test.com' }, {
  excludeExtraneousValues: true,
});
In this mode, only properties with @Expose() are kept.
Downside: you must decorate all properties you want to keep.
- 
strategy: 'exposeAll' 
@Expose({ strategy: 'exposeAll' })
class MobileSignupBody {
  email!: string;
  @Expose()
  @Transform(({ obj }) => obj.email)
  normalized_email?: string;
}
This makes all properties exposed by default, and you only need @Expose() where you want transforms.
Alternative: Use a Getter
Sometimes, the simplest solution is just a computed property:
class MobileSignupBody {
  email!: string;
  get normalized_email(): string | undefined {
    return this.email;
  }
}
No @Expose, no @Transform, always works.
Takeaways
@Transform only runs if the property exists in the plain object.
Use @Expose to force properties into the transformation pipeline.
For global behavior, use excludeExtraneousValues or strategy: 'exposeAll'. Or skip it all and use getters if you just want a derived property.
🔗 Useful refs:
class-transformer docs
class-validator docs
Have you ever hit this @Transform gotcha? How do you usually handle derived fields in DTOs — getters, transforms, or something else? Let me know 👇
    
Top comments (0)