class-validator
class-validator copied to clipboard
feature: make `NotMatches()`, a negation of `Matches()`
Description
I'm working on a project where the contents of a field must NOT match a regular expression. Making a negative regular expression to use with Matches()
would be too complex.
Proposed solution
Add a negation of Matches()
called NotMatches()
.
Until this is added to class-validator
natively, feel free to use my implementation in class-validator-extended
.
For additional context, in both #331 and #527 (both of which are about the same thing) the rejection reason is "just write the negated regexp". Without context, that'd be a sensible reason with which I'd agree. However, from my (arguably, rich) experience with regular expressions, it is often the case that a "negative" version is hard or even impossible to come by.
The reason is that there is no notion of a string "opposite" to a given one.
For example, if the pattern is /abc/
, it matches literally only "abc"
. That's a very simple pattern and a very simple case. But what would be the opposite pattern? Is it /cba/
? Or /xyz/
? Or maybe/[^a][^b][^c]/
? Most people would say that the "opposite" string would be any string that's not "abc"
, – but there's no pattern for that. The only way to express "any string other than abc
" (with a regex) is:
!input.match(/abc/)
… and the only way currently to make this kind of logic with class-validator
is to define a custom @NotMatches(…)
decorator:
import { registerDecorator, type ValidationOptions, matches } from 'class-validator'
/** @private */
type ArgsRegExp = [pattern: RegExp, options?: ValidationOptions]
/** @private */
type ArgsString = [pattern: string, modifiers?: string, options?: ValidationOptions]
/** @private */
type Args = ArgsRegExp | ArgsString
/** @private */
interface NotMatchesParams {
readonly pattern: RegExp
readonly options: ValidationOptions | undefined
}
/** @private */
function parseParams([arg0, arg1, arg2]: Args): NotMatchesParams {
if (arg0 instanceof RegExp) {
return {
pattern: arg0,
options: arg1 as ValidationOptions,
}
}
return {
pattern: new RegExp(arg0, arg1 as string),
options: arg2,
}
}
export function NotMatches(...args: ArgsRegExp): PropertyDecorator
export function NotMatches(...args: ArgsString): PropertyDecorator
export function NotMatches(...args: Args): PropertyDecorator {
const { pattern, options } = parseParams(args)
return (target, key): void => {
const propertyName = String(key)
registerDecorator({
async: false,
name: 'notMatches',
propertyName,
target: target.constructor,
options,
validator: {
validate(value: unknown) {
return typeof value === 'string' && !matches(value, pattern)
},
defaultMessage() {
return `${propertyName} must not match ${pattern} regular expression`
},
},
})
}
}