typestack / class-transformer

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

Feature request: plainToClass with discriminator #223

Open patricknazar opened 5 years ago

patricknazar commented 5 years ago

Is it possible for me to have a plainToClass(Vehicle, v) call return either a Car class or Train depending on it's type property? I know this is similar to discriminator in @Type. I don't like having to create a wrapper class just to harness that ability:

class Wrapper {
  @Type( () => Vehicle, {
    discriminator: {
      property: 'type',
      subTypes: [
        {value: Car, name: 'car'},
        {value: Train, name: 'train'},
      ],
    },
  })
  vehicle: Car | Train;
}

const w = plainToClass(Wrapper, { vehicle: { type: 'car' } });
const v = w.vehicle;
v.doSomething();
mitkoevoets commented 5 years ago

even though according to the documentation the above example should work, I was having issues with this too.

Is Vehicle an abstract class that is extended by Car and Train? I was having a similar issue and needed to replace the first argument abstract class with generic Object to make it work;

Like so:

class Wrapper {
  @Type( () => Object, {
    discriminator: {
      property: 'type',
      subTypes: [
        {value: Car, name: 'car'},
        {value: Train, name: 'train'},
      ],
    },
  })
  vehicle: Car | Train;
}
patricknazar commented 5 years ago

I'm not having that problem, I am simply asking for the ability to take a plain object and turn it into an instance of a class based on the type property without needing to use a wrapper class. But yeah Vehicle is an abstract class. It's not a super important thing but it would be nice.

alexpls commented 5 years ago

Hey @patricknazar, I've had a read through the code and can't see any way of using polymorphism directly on the root object being sent to plainToClass(). It looks like the polymorphism features only work for nested values in an object.

I reckon it would be more appropriate to retitle this issue as a feature request, rather than a question.

pellul commented 4 years ago

I think PR #175 solves this, it's not merged yet though.

nathanbabcock commented 2 years ago

This is a year old, any progress? For now I've written my own helper functions such as:

//// Effects
export const EffectTypeOptions: TypeOptions = {
  discriminator: {
    property: 'type',
    subTypes: [
      { value: Confetti, name: 'confetti' },
      { value: Clip, name: 'clip' },
      { value: Sparks, name: 'sparks' },
      { value: Stamp, name: 'stamp' },
      { value: Watermark, name: 'watermark' },
      { value: Toast, name: 'toast' },
    ]
  },
  keepDiscriminatorProperty: true,
}

export function plainToEffect(plain: unknown, options?: ClassTransformOptions): Effect {
  const type = (plain as any)[EffectTypeOptions.discriminator!.property]
  const subType = EffectTypeOptions.discriminator!.subTypes.find(subType => subType.name === type)
  if (!subType)
    throw new Error(`Could not find class constructor for type '${type}'`)
  return plainToClass(subType.value, plain, options)
}

However the logic is just re-expressing what class-transformer does internally for nested objects. It'd be great if this utility was built in, in a more abstract way.

arthabus commented 2 years ago

Would be really nice to have that.

In my case I have a collection of objects of various sub-types that I need to query from a NoSQL db.

Currently I have to create a wrapper for each collection and to also wrap each api call to apply that transformation which makes it cumbersome.

arthabus commented 2 years ago

Came up with the following generic approach for now which allows to use plainToClass with discriminator:

ClassTransformCustom.js

import {Type, plainToClass as plainToClassOriginal, classToPlain} from "class-transformer";

//helper factory function that constructs wrapper at runtime passing base class and discriminator
let transformWrapperFactory = function (baseClass, options){
    return class  {
        @Type(() => baseClass, options)
        data
    }
}

let plainToClass = function (clazz, obj, options = {}){
    let res
    if(options.discriminator){
        let objWrapper = {data: obj}
        let clazzWrapper = transformWrapperFactory(clazz, options)
        let resWrapper = plainToClassOriginal(clazzWrapper, objWrapper, options)
        res = resWrapper.data
    } else {
        res = plainToClassOriginal(clazz, obj, options)
    }
    return res
}

let classToClass = function (obj, options = {}){
    let plain = classToPlain(obj)
    return plainToClass(obj.constructor, plain, options)
}

export {
    plainToClass,
    classToPlain,
    classToClass,
}

How to use it:

import {plainToClass} from "./ClassTransformCustom.js"

let plainObj = {description: "some description", type: "NOTE"}

let instance = plainToClass(BaseClass, plainObj, {discriminator: yourDiscriminator})