How can I get class-validator
to be stricter when validating sub-types - specifically, reject invalid discriminator values, and not automatically look inside arrays?
Consider the following code:
import 'reflect-metadata';
import { Equals, ValidateNested, validateOrReject } from 'class-validator';
import { plainToClass, Type } from 'class-transformer';
class Base {
type: string;
value: string;
}
class Derived1 extends Base {
@Equals('value1')
value: string;
}
class Derived2 extends Base {
@Equals('value2')
value: string;
}
class Data {
@ValidateNested()
@Type(() => Base, {
keepDiscriminatorProperty: true,
discriminator: {
property: 'type',
subTypes: [
{ value: Derived1, name: 'derived1' },
{ value: Derived2, name: 'derived2' },
],
},
})
stuff: Base;
}
const validate = async (data: unknown) => {
const instance = plainToClass(Data, data);
await validateOrReject(instance);
};
(async () => {
validate({ stuff: { type: 'derived1', value: 'value1' } }); // (1) passes as expected
validate({ stuff: { type: 'derived2', value: 'value2' } }); // (2) passes as expected
validate({ stuff: { type: 'derived3', value: 'value3' } }); // (3) how can I get this to throw?
validate({ stuff: [{ type: 'derived1', value: 'value1' }] }); // (4) how can I get this to throw?
validate({ stuff: [{ type: 'derived1', value: 'value2' }] }); // (5) this throws, expecting 'value' to equal 'value1'
})();
In (3), it seems that class-validator
is perfectly happy with the invalid type
field that was passed. Perhaps it is actually the class-transformer
that should be expected to throw here?
In (4) I am actually surprised that this passes without any issue. Can I force class-validator
/ class-transformer
to not automatically validate each array element as if it were a Base
?
You can make your example work by using the @IsString()
, @IsIn()
and @IsObject()
decorators:
import 'reflect-metadata';
import { Equals, ValidateNested, validateOrReject } from 'class-validator';
import { plainToClass, Type } from 'class-transformer';
class Base {
@IsString()
@IsIn(['derived1', 'derived2'])
type: string;
value: string;
}
class Derived1 extends Base {
@Equals('value1')
value: string;
}
class Derived2 extends Base {
@Equals('value2')
value: string;
}
class Data {
@IsObject()
@ValidateNested()
@Type(() => Base, {
keepDiscriminatorProperty: true,
discriminator: {
property: 'type',
subTypes: [
{ value: Derived1, name: 'derived1' },
{ value: Derived2, name: 'derived2' },
],
},
})
stuff: Base;
}
const validate = async (data: unknown) => {
const instance = plainToClass(Data, data);
await validateOrReject(instance);
};
(async () => {
validate({ stuff: { type: 'derived1', value: 'value1' } }); // (1) passes as expected
validate({ stuff: { type: 'derived2', value: 'value2' } }); // (2) passes as expected
validate({ stuff: { type: 'derived3', value: 'value3' } }); // (3) how can I get this to throw?
validate({ stuff: [{ type: 'derived1', value: 'value1' }] }); // (4) how can I get this to throw?
validate({ stuff: [{ type: 'derived1', value: 'value2' }] }); // (5) this throws, expecting 'value' to equal 'value1'
})();
So:
IsObject()
you are forcing stuff
to be an object and your forbid arrays.IsIn()
you are forcing type
to be one of your options ('derived1' | 'derived2'
), anything else will fail.keepDiscriminatorProperty: true
to make it work, if not, @IsIn
will fail since type
key won't be present on type
key validation process.