Custom Validator of Map Keys #2064
Unanswered
dieeisenefaust
asked this question in
Questions & Answers
Replies: 1 comment
-
Updated the logic to to be more generic, so users can feed it the enum type they wish to validate against along with logic to verify the constraint parameter was provided. @ValidatorConstraint({ name: "customMapKeyValidator", async: false })
export class customMapKeyValidator implements ValidatorConstraintInterface {
validate(map: Map<string, string>, args: ValidationArguments) {
let validationAnswer = true;
if (args.constraints && args.constraints[0] instanceof Object) {
map.forEach((value, key) => {
if (!(key in args.constraints[0])) {
validationAnswer = false;
}
});
} else {
validationAnswer = false;
}
//console.log(validationAnswer);
return validationAnswer; // for async validations you must return a Promise<boolean> here
}
defaultMessage(args: ValidationArguments) {
const invalidKeys: Array<string> = [];
const map: Map<string, string> = args.value;
if (args.constraints && args.constraints[0] instanceof Object) {
map.forEach((value, key) => {
if (!(key in args.constraints[0])) {
invalidKeys.push(key);
}
});
} else {
return "Constraint parameter not provided or was not an Enum.";
}
if (invalidKeys.length === 1) {
return `Key [${invalidKeys}] is not an accepted value.`;
} else {
return `Keys [${invalidKeys}] are not accepted values.`;
}
}
} Usage: //...see code in previous post for PhoneType enum
export enum Roles {
Manager = "Manager",
Employee = "Employee",
}
class User {
...
@IsOptional({ groups: ["create"] })
@MaxLength(12, { each: true, groups: ["create"] })
@Validate(customMapKeyValidator, [PhoneType], { groups: ["create"] })
@Type(() => String)
@Transform(({ value }) => new Map(value), { toClassOnly: true })
@Transform(({ value }) => JSON.stringify(Array.from(value.entries())), {
toPlainOnly: true,
})
public phoneNumbers: Map<PhoneType, string>;
@IsOptional({ groups: ["create"] })
@Validate(customMapKeyValidator, [Roles], { groups: ["create"] })
@Type(() => String)
@Transform(({ value }) => new Map(value), { toClassOnly: true })
@Transform(({ value }) => JSON.stringify(Array.from(value.entries())), {
toPlainOnly: true,
})
public roles: Map<Roles, string>;
} Output: [
ValidationError {
target: User {
username: 'example1',
firstName: 'Kevin',
lastName: 'Smith',
password: '[***]',
phoneNumbers: Map(3) {
'Home' => '123-456-7890',
'Cell' => '098-765-4321',
'TV' => '098-765-4321'
},
roles: Map(2) {
'Manager' => 'ref1',
'Temp' => 'ref1'
}
},
value: {},
property: 'phoneNumbers',
children: [],
constraints: {
customMapKeyValidator: 'Keys [Cell,TV] are not accepted values.'
}
},
ValidationError {
target: {
username: 'example1',
firstName: 'Kevin',
lastName: 'Smith',
password: 'thisisatest',
phoneNumbers: Map(3) {
'Home' => '123-456-7890',
'Cell' => '098-765-4321',
'TV' => '098-765-4321'
},
roles: Map(2) {
'Manager' => 'ref1',
'Temp' => 'ref1'
}
},
value: {},
property: 'roles',
children: [],
constraints: {
customMapKeyValidator: 'Key [Temp] is not an accepted value.'
}
}
] |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
I have not found any other documentation on this and the one closed issue I found simply stated that class-validator did not validate keys.
So I decided to post this solution for validating map keys which are of type Enum and post an oustanding question I have about custom validators as well.
Maps obviously allow for dynamic key names, but canned validators look at the values, not the keys.
This means, if you want to use a canned validator, you have to use something like an Array or Object and store a 'type' and 'value' key/value pairs separately.
Using phone numbers as an example, you might have to have something like:
Whereas, using a Map, you could do
Not only is your code shorter, but your JSON/Object will be as well:
Using subclass:
Using Map:
Using the custom validator constraint logic, you are able to access to several args including the entire object and the specific property being evaluated. Therefore, validating the key comes down to casting the value parameter as a
Map<>
and then looping through it to ensure each key matches a value in your chosen Enum and returning false if one or more do not.The same for the defaultMessage method; loop through the phoneNumber entries in the phoneNumber Map and build an array of keys that are invalid which you can then use in your custom message.
This leads me to the outstanding question I have: Can anyone think of a better way to share the invalid keys from the
validate
method with thedefaultMessage
method? As it stands right now, I have to iterate over the Map in each method separately instead of building the list of invalid keys only once in thevalidate
method and then accessing it in thedefaultMessage
method. The class is only instantiated once, so you can't have some private array as it will be shared by/written to by every call to the validator. Just seems inefficient.I haven't build a custom decorator for it yet, but using this on the
phoneNumbers
parameter in theUser
class looks like this:And the example output looks like:
Validation failed:
Validation Successful allows User to be created:
Beta Was this translation helpful? Give feedback.
All reactions