Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add decorator to check if two properties match #486

Open
mrpharderwijk opened this issue Dec 9, 2019 · 17 comments · May be fixed by #2462
Open

feat: add decorator to check if two properties match #486

mrpharderwijk opened this issue Dec 9, 2019 · 17 comments · May be fixed by #2462
Labels
type: feature Issues related to new features.

Comments

@mrpharderwijk
Copy link

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;
}
@rubiin
Copy link
Contributor

rubiin commented Dec 15, 2019

@vlapo

@rubiin
Copy link
Contributor

rubiin commented Dec 20, 2019

@vlapo how can we achieve something like this??

@vlapo
Copy link
Contributor

vlapo commented Jan 10, 2020

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

@vlapo vlapo added the type: feature Issues related to new features. label Jan 10, 2020
@joelcoronah
Copy link

Hi guys, any update @vlapo?

@sevosevo
Copy link

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

@PieroMacaluso
Copy link

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!

@mrpharderwijk
Copy link
Author

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`;
  }
}

@NoNameProvided NoNameProvided changed the title How do I check if two values match? feat: add decorator to check if two properties match Aug 8, 2020
@gimerstedt
Copy link

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,
    });
  };
}

@LeonardoRosaa
Copy link

@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`;
    }
}

@hnbnh
Copy link

hnbnh commented Jul 28, 2021

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;

@allanvobraun
Copy link

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!

@bato3
Copy link

bato3 commented Jan 16, 2022

@hnbnh Cam you show ClassConstructor definition?

@hnbnh
Copy link

hnbnh commented Jan 17, 2022

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

import { ClassConstructor } from "class-transformer";

@kuriel-trivu
Copy link

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`;
  }
}

@ghost
Copy link

ghost commented Nov 7, 2022

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

@peixotoleonardo
Copy link

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`;
  }
}

@huzaifarif
Copy link

huzaifarif commented Nov 11, 2023

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!

MamadTvl added a commit to MamadTvl/class-validator that referenced this issue Apr 14, 2024
MamadTvl added a commit to MamadTvl/class-validator that referenced this issue Apr 14, 2024
MamadTvl added a commit to MamadTvl/class-validator that referenced this issue Apr 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: feature Issues related to new features.