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

fix: Exposing properties with different names does not work with nested objects.

Open dilizarov opened this issue 3 years ago • 10 comments

Description

I'm trying to expose a custom property on a nested object, but it returns undefined instead of calling the getter.

Minimal code-snippet showcasing the problem

@Exclude()
class SubEntity {
  @Expose()
  @Transform(({ obj }) => obj.name.toUpperCase())
  working: string;

  @Expose()
  name: string;

  @Expose({ name: "broken" })
  get entity() {
    return this.name.toUpperCase();
  }
}
@Exclude()
export class Test {
  @Expose({ name: "name" })
  getName() {
    return "This gets exposed";
  }

  @Expose()
  @Type(() => SubEntity)
  entity: SubEntity;

  constructor() {
    Object.assign(this, { entity: { name: "name" } });
  }
}

new ClassTransformer().classToPlain(new Test());

Expected behavior

I expect the following output:

{
  name: 'This gets exposed',
  entity: { working: 'NAME', name: 'name', broken: 'NAME' }
}

Actual behavior

This is what I get:

{
  name: 'This gets exposed',
  entity: { working: 'NAME', name: 'name', broken: undefined }
}

This is a bug and forces me to use @Transform instead of @Expose as desired.

dilizarov avatar May 07 '21 14:05 dilizarov

I have some problem with you. Please show me @Transform decorator usage for custom different property name.

muh-hizbe avatar Jun 16 '21 15:06 muh-hizbe

Confirmed, it's broken

cojack avatar Sep 15 '21 13:09 cojack

Not only nested object, but even direct objects exposed under a different name are undefined.

I wanted to just directly expose id as _id for mongoose/mongodb sake.

This is the workaround I came up with, downside being that both properties are now present with the same value on the object when only one is needed.

export class FreelancerSortInput extends TimeStampsSortInput {
  @IsEnum(Sort)
  @IsOptional()
  @Transform(o => o.obj.id)
  @Expose()
  _id?: Sort

  @Field(() => Sort, {
    nullable: true,
    description: 'Sort by ID either ascending (ASC) or descrending (DESC)',
  })
  @IsEnum(Sort)
  @IsOptional()
  id?: Sort
}

calebpitan avatar Oct 13 '21 05:10 calebpitan

Any update on this issue ?

I have the same issue when exposing nested objects in an array.

nvs2394 avatar Oct 19 '21 08:10 nvs2394

Facing the exact same issue, is it still borken?

hsellik avatar Feb 11 '22 14:02 hsellik

Same issue. The only workaround is to use transform instead of @Type which is not ideal.

BlakeB415 avatar May 30 '22 03:05 BlakeB415

As @calebpitan mentioned, even direct objects exposed under a different name are undefined too. Using the example in the README, id will be undefined if uid isn't a property in the source object:

export class User {
  @Expose({ name: 'uid' })
  id: number;
}

However, this will work using Transform with id taking on uid's value:

export class User {
  @Transform(({ obj, value }) => value ? value : obj.uid))
  id: number;
}

And if uid isn't a property on the source object, id will still have a value.

uythoang avatar Dec 22 '22 02:12 uythoang

Broken confirmed.

I use the following to transform a JWT with its shorthand properties to more human-readable properties. These shorthand properties should be transformed into the 3-character properties when transforming to plain, so serialization will mirror the original input.

This works fine when MemberJwt is transformed by itself, but when nested inside the Session class, things fall apart.

import 'reflect-metadata'
import { Expose, instanceToPlain, plainToInstance, Type } from 'class-transformer'

class MemberJwt {

  @Expose({ name: 'iss' })
  issuser: string

  @Expose({ name: 'sub' })
  subject: string
}

class Session {

  id: string

  @Type(() => MemberJwt)
  member: MemberJwt
}

const member = plainToInstance(MemberJwt, { iss: 'test', sub: '123' })
const memberPlain = instanceToPlain(member)
const session = plainToInstance(Session, { id: '333', member })

it('should transform member JWT to instance using @Expose aliases', () => {
  expect(member.issuser).toBe('test')
  expect(member.subject).toBe('123')
})

it('should transform member JWT to shorthand props', () => {
  expect(memberPlain.iss).toBe('test')
  expect(memberPlain.sub).toBe('123')
})

it('should retain member JWT instance properties within transformed Session instance', () => {
  expect(session.id).toBe('333')
  expect(session.member).toBeDefined()
  expect(session.member.issuser).toBe('test')
  expect(session.member.subject).toBe('123')
})

it('should not retain @Expose alias properties within transformed Session instance', () => {
  expect(session.member['iss']).toBeUndefined()
  expect(session.member['sub']).toBeUndefined()
})

The fix I found is to not use @Expose at all, and use getters:

class MemberJwt {

  private iss: string
  private sub: string

  get issuser(): string {
    return this.iss
  }

  get subject(): string {
    return this.sub
  }
}

nolawnchairs avatar Mar 04 '23 07:03 nolawnchairs

Had the same issue, using getters worked for me. Thanks @nolawnchairs

hsulipe avatar Jul 14 '23 15:07 hsulipe

This works for me when I invert the name of the property with the options name for the Expose decorator:

@Expose({ name: 'originalPropertyName'})
newPropertyName: number;

RobbyKetchell avatar Oct 26 '23 05:10 RobbyKetchell