-
-
Notifications
You must be signed in to change notification settings - Fork 572
Description
Bug Report
Current behavior
When serializing a DTO that contains a polymorphic field decorated with @Type({ discriminator: ... }), the library performs the following transformation sequence (
| : classToPlain(classToPlainFromExist(data, new dto()), options); |
classToPlain(
classToPlainFromExist(data, new Dto()),
options
);The first call to classToPlainFromExist converts the source object (data) into a plain object using the source class’s metadata, then merges it into a new Dto instance.
As a result, any nested class instances (including polymorphic ones) are replaced by plain objects.
When the second call to classToPlain executes, the polymorphic field is no longer an instance, so the @Type decorator’s discriminator mapping does not apply.
This causes subtype-specific transformations to be skipped and results in incorrect or incomplete serialization output.
Input Code
import { Expose, Type, classToPlain, classToPlainFromExist } from 'class-transformer';
class Animal {
@Expose() type!: 'cat' | 'dog';
}
class Cat extends Animal {
@Expose() meow!: string;
}
class Dog extends Animal {
@Expose() bark!: string;
}
class OwnerDto {
@Expose()
name!: string;
@Expose()
@Type(() => Animal, {
discriminator: {
property: 'type',
subTypes: [
{ value: Cat, name: 'cat' },
{ value: Dog, name: 'dog' },
],
},
keepDiscriminatorProperty: true,
})
pet!: Cat | Dog;
}
const data = {
name: 'Alice',
pet: Object.assign(new Cat(), { type: 'cat', meow: 'purr' }), // Instance
};
const dto = OwnerDto;
// Problematic code path:
const intermediate = classToPlainFromExist(data, new dto());
const result = classToPlain(intermediate);
console.log(result);
// `pet` is now plain, discriminator not appliedExpected behavior
The @Type({ discriminator: ... }) decorator should correctly identify subtype instances (Cat, Dog) and apply their transformations during serialization.
The final output should preserve the polymorphic subtype structure and fields as defined by the DTO.
Possible Solution
Avoid flattening instances before DTO serialization.
Instead, preserve instance types until the final conversion step.
Recommended fix (modern API)
import { plainToInstance, instanceToPlain } from 'class-transformer';
const dtoInstance = plainToInstance(OwnerDto, data);
const json = instanceToPlain(dtoInstance, options);Alternate fix (for instance input)
import { instanceToPlain } from 'class-transformer';
const dtoInstance = Object.assign(new OwnerDto(), data);
const json = instanceToPlain(dtoInstance, options);Key point:
classToPlainFromExist should not be called before classToPlain. It converts nested instances to plain objects too early, breaking polymorphic discriminator behavior. The names of these functions make it very clear whether they accept and return a plain object or a class instance, so we know that the result of classToPlainFromExist is a plain object whereas classToPlain expects a class instance.
Environment
Package version: @nestjsx/crud: X.Y.Z
class-transformer: 0.4.x / 0.5.x (confirmed on both)
Node version: vXX.Y.Z
Platform: macOS/Linux/Windows
Database: N/A
Framework: NestJS
Note:
This issue originates from the pattern:
classToPlainFromExist(..., new Dto()) → classToPlain(...)
Repository with minimal reproduction
-
Define
Animal,Cat,Dog, andOwnerDtoas above. -
Create a test object with a
Catinstance:const data = { name: 'Alice', pet: Object.assign(new Cat(), { type: 'cat', meow: 'purr' }) };
-
Run:
const intermediate = classToPlainFromExist(data, new OwnerDto()); const result = classToPlain(intermediate);
-
Observe that
petis a plain object without discriminator behavior. -
Replace with:
const dtoInstance = plainToInstance(OwnerDto, data); const ok = instanceToPlain(dtoInstance);
-
Observe that the discriminator applies correctly and subtype transforms work as expected.