Skip to content

Querying an entity that has a polymorphic type in a field throws error TypeError: Cannot read properties of undefined (reading 'name') #845

@Woodz

Description

@Woodz

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 applied

Expected 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

  1. Define Animal, Cat, Dog, and OwnerDto as above.

  2. Create a test object with a Cat instance:

    const data = { name: 'Alice', pet: Object.assign(new Cat(), { type: 'cat', meow: 'purr' }) };
  3. Run:

    const intermediate = classToPlainFromExist(data, new OwnerDto());
    const result = classToPlain(intermediate);
  4. Observe that pet is a plain object without discriminator behavior.

  5. Replace with:

    const dtoInstance = plainToInstance(OwnerDto, data);
    const ok = instanceToPlain(dtoInstance);
  6. Observe that the discriminator applies correctly and subtype transforms work as expected.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions