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

fix: instanceToInstance fails for discriminator with null value - Cannot read properties of null (reading 'constructor')

Open rat-matheson opened this issue 2 years ago • 4 comments

Description

When using instanceToInstance on an object that has a discriminated property with a null value, and exception gets thrown. Note that plainToInstance works fine

Minimal code-snippet showcasing the problem

Here is a repository with a test showing the issue.

import { Exclude, Expose, instanceToInstance, plainToInstance, Type } from "class-transformer";

export class FullName {
    public first?:string;
    public last?:string;
} 

export class LockedValue { 
    private readonly value:string;
    private readonly __type = "LockedValue";
    
    constructor(value:string) { 
        this.value = value;
    }

    public toString():string {
        return this.value;
    }
}

@Exclude()
export class User {
    // My use case is I want to default to FullName object if no discriminator is present.  Otherwise, I want to create an instance of LockedValue
    @Type(() => FullName,  {
        keepDiscriminatorProperty: true,
        discriminator: {
            property: "__type",
            subTypes: [
                { value: LockedValue, name: "LockedValue" },
            ]
        }
    })
    @Expose()
    public fullName?:FullName | LockedValue;
}


describe('class-transformer @Type discriminator', () => {
    it('should create a copy using plainToInstance when discrimnated field is null', async () => {
        let u = new User();
        u.fullName = null;
        
        // works fine
        let copy = plainToInstance(User, u);
        expect(copy == u).toBeFalse();
    })

    it('should create a copy using instanceToInstance when discrimnated field is null', async () => {
        let u = new User();
        u.fullName = null;

        // throws exception Cannot read properties of null (reading 'constructor')
        let copy = instanceToInstance(u);

        expect(copy == u).toBeFalse();
    })
})

Expected behavior

expect no exception

Actual behavior

Error details

  Message:
    TypeError: Cannot read properties of null (reading 'constructor')
  Stack:
        at TransformOperationExecutor.transform (C:\Users\joelw\eclipse-workspace\tower-poi\packages\bug-reports\node_modules\src\TransformOperationExecutor.ts:244:35)
        at ClassTransformer.instanceToInstance (C:\Users\joelw\eclipse-workspace\tower-poi\packages\bug-reports\node_modules\src\ClassTransformer.ts:113:21)
        at Object.instanceToInstance (C:\Users\joelw\eclipse-workspace\tower-poi\packages\bug-reports\node_modules\src\index.ts:106:27)
        at UserContext.<anonymous> (C:\Users\joelw\eclipse-workspace\tower-poi\packages\bug-reports\projects\class-transformer-issues\src\tst\discriminator.spec.ts:51:20)
        at <Jasmine>

rat-matheson avatar Jun 03 '22 19:06 rat-matheson

As a side note, my use case involves union discrimination rather than inheritance. I don't think it really changes anything with regards to this issue but worth mention. It is a security feature used for wrapping data that a client is not allowed to view

rat-matheson avatar Jun 03 '22 20:06 rat-matheson

I just hit same issue

karlismelderis-mckinsey avatar Jun 06 '23 12:06 karlismelderis-mckinsey

Here is my example

import "reflect-metadata";
import {
  Expose,
  instanceToInstance,
  Type
} from "class-transformer";

import { IsString } from "class-validator";

class SubDto {
  @Expose()
  @IsString()
  str: string;
}

class SubExtendedADto extends SubDto {
  @Expose()
  @IsString()
  extendedA: string;
}

class SubExtendedBDto extends SubDto {
  @Expose()
  @IsString()
  extendedB: string;
}

class MainDto {
  @Expose()
  @Type(() => SubDto, {
    discriminator: {
      property: 'str',
      subTypes: [
        { value: SubExtendedADto, name: 'A' },
        { value: SubExtendedBDto, name: 'B' }
      ],
    },
    keepDiscriminatorProperty: true,
  })
  sub: SubDto | null = null;
}

const dto = new MainDto();
const obj = instanceToInstance(dto);

karlismelderis-mckinsey avatar Jun 06 '23 12:06 karlismelderis-mckinsey