typestack / class-transformer

Decorator-based transformation, serialization, and deserialization between objects and classes.
MIT License
6.84k stars 500 forks source link

feature: extends discriminator lookup #857

Open getlarge opened 3 years ago

getlarge commented 3 years ago

Description

I truly appreciate the possibility to have pseudo dynamic class in my models. Unfortunately, it's not always possible to add a new specific property to make the lookup working and moreover quite often the discriminator can already be found in the parent object.

export class SignTransaction {
  @Type(() => Identity)
  @ValidateNested()
  identity: Identity;

  @Type(() => LedgerTransaction, {
    discriminator: {
      property: ['identity', 'coinType'],
      parentProperty: true,
      subTypes: [
        { value: BigChainTransaction, name: 822 },
        { value: EthereumTransaction, name: 60 },
      ],
    },
    keepDiscriminatorProperty: true,
  })
  @ValidateNested()
  transaction: BigChainTransaction | EthereumTransaction;
}

Proposed solution

Relates to :

jonasof commented 2 years ago

Tip: While that solutions are not implemented I'm using the generic @Transform decorator to convert to the specific classes. Example with top-level param:

export abstract class Photo {
  id: number;
  filename: string;
}

export class Landscape extends Photo {
  panorama: boolean;
}

export class Portrait extends Photo {
  person: Person;
}

export class UnderWater extends Photo {
  depth: number;
}

enum TopPhotoType {
  'landscape' = 'landscape',
  'portrait' = 'portrait',
  'underwater' = 'underwater'
}

const topPhotoTypeMap:  { [key in TopPhotoType]: new () => Photo } = {
  'landscape': Landscape,
  'portrait': Portrait,
  'underwater': UnderWater
}

export class Album {
  id: number;
  name: string;

  topPhotoType: TopPhotoType;

  @Transform(
    ({ value, obj }) => {
      const specificClass = topPhotoTypeMap[obj.topPhotoType];

      return plainToInstance(specificClass, value);
    },
    {
      toClassOnly: true
    }
  )
  @Type(() => Object)
  topPhoto: Landscape | Portrait | UnderWater;
}

let album = plainToClass(Album, albumJson);
getlarge commented 1 year ago

To make @jonasof solution even more useful i suggest to also resolve the dynamic type so that @ValidateNested can also be used. To continue with his example:

export abstract class Photo {
  id: number;
  filename: string;
}

export class Landscape extends Photo {
  panorama: boolean;
}

export class Portrait extends Photo {
  person: Person;
}

export class UnderWater extends Photo {
  depth: number;
}

enum TopPhotoType {
  'landscape' = 'landscape',
  'portrait' = 'portrait',
  'underwater' = 'underwater'
}

const topPhotoTypeMap:  { [key in TopPhotoType]: new () => Photo } = {
  'landscape': Landscape,
  'portrait': Portrait,
  'underwater': UnderWater
}

export class Album {
  id: number;
  name: string;

  topPhotoType: TopPhotoType;

  @Transform(
    ({ value, obj }) => {
      const specificClass = topPhotoTypeMap[obj.topPhotoType];
      return plainToInstance(specificClass, value);
    },
    {
      toClassOnly: true
    }
  )
  @Type(({ object }) => topPhotoTypeMap[object?.topPhotoType])
  @ValidateNested()
  topPhoto: Landscape | Portrait | UnderWater;
}

let album = plainToClass(Album, albumJson);