Search code examples
nestjsclass-validatorclass-transformer

Using @Type discriminator with class-validator and class-transform not working in tandem


I have a class with a property on it, which can be a number of classes based on a property. @Type obviously is perfect for this, but the issue is, the discriminator does not exist on the object, it exists on the parent.

Consider the following:

class Parent {
  type: 'a' | 'b'

  @Type(() => ?, {
    discriminator: {
      property: 'type',
      subTypes: [
        { value: TypeA, name: 'a' },
        { value: TypeB, name: 'b' },
      ]    
    }
  }
  data: TypeA | TypeB

}

Naturally I can't do this. I tried a custom decorator which does something like:

const TypeOnParent: () => PropertyDecorator = () => {
  const __class__ = class {}
  const prop = '__type'

  return (target, key) => {
    Transform(({ value, obj }) => {
      value[prop] = obj.type
      return value
    })(target, key)

    Type(() => __class__, {
      keepDiscriminatorProperty: true,
      discriminator: {
        property: prop,
        subTypes: [
          { name: 'a', value: TypeA },
          { name: 'b', value: TypeB },
        ],
      },
    })(target, key)
  }
}

class Parent {
  type: 'a' | 'b'

  @TypeOnParent('type')
  data: TypeA | TypeB

}

The goal here is to pass the parent prop onto the child, so that Type discriminator can do its job. However, the discriminator prop that I pass onto the child 'data' prop doesn't seem to work. It just defaults to using the class instance. I've tried changing the order of the decorators.

The result is an identical object no matter what, but if I pass the value in manually via payload, it works fine. If I use the transform, it never works.

Am I missing something? Does class-transform ALWAYS run type before any other decorators? Or is there a better way to achieve this? I am using nestjs global validation pipe if that helps.


Solution

  • I have solved this not by using discriminator, but the type function.

    Type will always run first, before Transform, so Transform is not an option. However, it passes an object into the function, so we can simply use that:

     @Type((opts) => opts.object.type === 'a' ? TypeA : TypeB)
      data: TypeA | TypeB