class-validator icon indicating copy to clipboard operation
class-validator copied to clipboard

feat: add decorator to check if two properties match

Open mrpharderwijk opened this issue 6 years ago • 20 comments

First of all, thanks for the awesome validation solution! I use class-validator in my NestJS setup. Now I want to know if and how it is possible to check if two values match with eachother. Let's say I have a dto setup like this:


export class UserCreateDto {
  @IsString()
  @IsNotEmpty()
   firstName: string;

   @IsEmail()
   emailAddress: string;

   @DoesMatch(o => o.emailAddress === o.emailAddressConfirm) // <---- check if email's match
   @IsEmail()
   emailAddressConfirm: string;
}

mrpharderwijk avatar Dec 09 '19 07:12 mrpharderwijk

@vlapo

rubiin avatar Dec 15 '19 18:12 rubiin

@vlapo how can we achieve something like this??

rubiin avatar Dec 20 '19 07:12 rubiin

We do not have validator for this. Feel free open PR.

vlapo avatar Jan 10 '20 12:01 vlapo

Hi guys, any update @vlapo?

joelcoronah avatar Feb 28 '20 15:02 joelcoronah

Is there a way to make custom validator get access to other properties

sevosevo avatar Mar 22 '20 15:03 sevosevo

Hi! I am posting there the solution I found to implement this control. It refers to my question/answer on StackOverflow. I am going to provide a suitable solution for this particular issue.

user-create.dto.ts

export class UserCreateDto {
   @IsString()
   @IsNotEmpty()
   firstName: string;

   @IsEmail()
   emailAddress: string;

   @Match('emailAddress')
   @IsEmail()
   emailAddressConfirm: string;
}

match.decorator.ts

import {registerDecorator, ValidationArguments, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface} from 'class-validator';

export function Match(property: string, validationOptions?: ValidationOptions) {
    return (object: any, propertyName: string) => {
        registerDecorator({
            target: object.constructor,
            propertyName,
            options: validationOptions,
            constraints: [property],
            validator: MatchConstraint,
        });
    };
}

@ValidatorConstraint({name: 'Match'})
export class MatchConstraint implements ValidatorConstraintInterface {

    validate(value: any, args: ValidationArguments) {
        const [relatedPropertyName] = args.constraints;
        const relatedValue = (args.object as any)[relatedPropertyName];
        return value === relatedValue;
    }

}

I managed to solve a similar problem on my personal project. Hope it helps!

PieroMacaluso avatar Mar 31 '20 17:03 PieroMacaluso

Added a custom message to your solution like this:

@ValidatorConstraint({name: 'Match'})
export class MatchConstraint implements ValidatorConstraintInterface {

    validate(value: any, args: ValidationArguments) {
        const [relatedPropertyName] = args.constraints;
        const relatedValue = (args.object as any)[relatedPropertyName];
        return value === relatedValue;
    }

   defaultMessage(args: ValidationArguments) {
     const [relatedPropertyName] = args.constraints;
     return `${relatedPropertyName} and ${args.property} don't match`;
  }
}

mrpharderwijk avatar Apr 22 '20 18:04 mrpharderwijk

force the property to exist on the class:

export function Match<K extends string, T extends { [$K in K]: any }>(
  property: K,
  validationOptions?: ValidationOptions,
) {
  return (object: T, propertyName: string) => {
    registerDecorator({
      target: object.constructor,
      propertyName,
      options: validationOptions,
      constraints: [property],
      validator: MatchConstraint,
    });
  };
}

gimerstedt avatar Nov 24 '20 12:11 gimerstedt

@mrpharderwijk and @pieromacaluso, it's works for me. Thanks!!

Full code:

import {registerDecorator, ValidationArguments, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface} from 'class-validator';

export function Match(property: string, validationOptions?: ValidationOptions) {
    return (object: any, propertyName: string) => {
        registerDecorator({
            target: object.constructor,
            propertyName,
            options: validationOptions,
            constraints: [property],
            validator: MatchConstraint,
        });
    };
}

@ValidatorConstraint({name: 'Match'})
export class MatchConstraint implements ValidatorConstraintInterface {

    validate(value: any, args: ValidationArguments) {
        const [relatedPropertyName] = args.constraints;
        const relatedValue = (args.object as any)[relatedPropertyName];
        return value === relatedValue;
    }

    defaultMessage(args: ValidationArguments) {
      const [relatedPropertyName] = args.constraints;
      return `${relatedPropertyName} and ${args.property} don't match`;
    }
}

LeonardoRosaa avatar Dec 15 '20 01:12 LeonardoRosaa

Thanks everyone for a good solution, but there's a problem with type linting.

We could make a spelling mistake like:

@Match('passwordd')
//              👆

So I would like to make it more strict

import { ClassConstructor } from "class-transformer";

export const Match = <T>(
  type: ClassConstructor<T>,
  property: (o: T) => any,
  validationOptions?: ValidationOptions,
) => {
  return (object: any, propertyName: string) => {
    registerDecorator({
      target: object.constructor,
      propertyName,
      options: validationOptions,
      constraints: [property],
      validator: MatchConstraint,
    });
  };
};

@ValidatorConstraint({ name: "Match" })
export class MatchConstraint implements ValidatorConstraintInterface {
  validate(value: any, args: ValidationArguments) {
    const [fn] = args.constraints;
    return fn(args.object) === value;
  }

  defaultMessage(args: ValidationArguments) {
    const [constraintProperty]: (() => any)[] = args.constraints;
    return `${constraintProperty} and ${args.property} does not match`;
  }
}

Usage:

@Match(SignUpDto, (s) => s.password)
passwordConfirm: string;

hnbnh avatar Jul 28 '21 17:07 hnbnh

Thanks everyone for a good solution, but there's a problem with type linting.

We could make a spelling mistake like:

@Match('passwordd')
//              👆

So I would like to make it more strict

export const Match = <T>(
  type: ClassConstructor<T>,
  property: (o: T) => any,
  validationOptions?: ValidationOptions,
) => {
  return (object: any, propertyName: string) => {
    registerDecorator({
      target: object.constructor,
      propertyName,
      options: validationOptions,
      constraints: [property],
      validator: MatchConstraint,
    });
  };
};

@ValidatorConstraint({ name: "Match" })
export class MatchConstraint implements ValidatorConstraintInterface {
  validate(value: any, args: ValidationArguments) {
    const [fn] = args.constraints;
    return fn(args.object) === value;
  }

  defaultMessage(args: ValidationArguments) {
    const [constraintProperty]: (() => any)[] = args.constraints;
    return `${constraintProperty} and ${args.property} does not match`;
  }
}

Usage:

@Match(SignUpDto, (s) => s.password)
passwordConfirm: string;

Really good! This example should be in the docs!

allanvobraun avatar Dec 09 '21 11:12 allanvobraun

@hnbnh Cam you show ClassConstructor definition?

bato3 avatar Jan 16 '22 13:01 bato3

@bato3 I forgot to mention it, you have to import it from "class-transformer" like so:

import { ClassConstructor } from "class-transformer";

hnbnh avatar Jan 17 '22 01:01 hnbnh

Just a bit improvements, if you like it:

import {
  registerDecorator,
  ValidationArguments,
  ValidationOptions,
  ValidatorConstraint,
  ValidatorConstraintInterface,
} from 'class-validator';
import {ClassConstructor} from 'class-transformer';

export const MatchesWithProperty = <T>(
  type: ClassConstructor<T>,
  property: (o: T) => any,
  validationOptions?: ValidationOptions,
) => {
  return (object: any, propertyName: string) => {
    registerDecorator({
      target: object.constructor,
      propertyName,
      options: validationOptions,
      constraints: [property],
      validator: MatchConstraint,
    });
  };
};

@ValidatorConstraint({name: 'Match'})
export class MatchConstraint implements ValidatorConstraintInterface {
  validate(value: any, args: ValidationArguments) {
    const [fn] = args.constraints;
    return fn(args.object) === value;
  }
  
  defaultMessage(args: ValidationArguments) {
    const [constraintProperty]: Array<() => any> = args.constraints;
    return `${(constraintProperty + '').split('.')[1]} and ${args.property} does not match`;
  }
}

kuriel-trivu avatar Jun 28 '22 22:06 kuriel-trivu

This should be in-house I guess. The solution of @PieroMacaluso is very nice.

ghost avatar Nov 07 '22 12:11 ghost

We can receive the name of the property like string, to avoid typo we can use keyof:

import {
  ValidationArguments,
  ValidatorConstraint,
  ValidatorConstraintInterface,
  equals,
} from 'class-validator';

export const Match =
  <T>(property: keyof T, options?: ValidationOptions) =>
  (object: unknown, propertyName: string) =>
    registerDecorator({
      target: object.constructor,
      propertyName,
      options,
      constraints: [property],
      validator: MatchConstraint,
    });
    
@ValidatorConstraint({ name: 'Match' })
export class MatchConstraint implements ValidatorConstraintInterface {
  validate(value: any, validationArguments?: ValidationArguments): boolean {
    return equals(validationArguments.constraints[0], value);
  }

  defaultMessage(validationArguments?: ValidationArguments): string {
    return `${validationArguments.constraints[0]} and ${validationArguments.property} does not match`;
  }
}


  defaultMessage(validationArguments?: ValidationArguments): string {
    const [propertyNameToCompare] = validationArguments.constraints;

    return `${validationArguments.property} and ${propertyNameToCompare} does not match`;
  }
}

peixotoleonardo avatar Jan 15 '23 13:01 peixotoleonardo

Some slight corrections to @peixotoleonardo's really nice approach above, the complete code looks like this:

Validator:

import {
  equals,
  ValidationArguments,
  ValidationOptions,
  ValidatorConstraint,
  ValidatorConstraintInterface,
  registerDecorator,
} from 'class-validator';

export const Match =
  <T>(property: keyof T, options?: ValidationOptions) =>
  (object: any, propertyName: string) =>
    registerDecorator({
      target: object.constructor,
      propertyName,
      options,
      constraints: [property],
      validator: MatchConstraint,
    });

@ValidatorConstraint({ name: 'Match' })
export class MatchConstraint implements ValidatorConstraintInterface {
  validate(value: any, args?: ValidationArguments): boolean {
    const [propertyNameToCompare] = args?.constraints || [];
    const propertyValue = (args?.object as any)[propertyNameToCompare];
    return equals(value, propertyValue);
  }

  defaultMessage(args?: ValidationArguments): string {
    const [propertyNameToCompare] = args?.constraints || [];

    return `${args?.property} does not match the ${propertyNameToCompare}`;
  }
}

Usage:

@Match<SignupDto>('password')
confirmPassword: string;

Thanks @hnbnh and other for the solutions, this works like a charm!

huzaifarif avatar Nov 11 '23 11:11 huzaifarif