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

feat: add option to define class level custom decorators

Open j opened this issue 6 years ago • 27 comments

Is there support for validation on a class level and not just property level? I want to do DB lookups but issue a single query vs one per field.

j avatar Mar 22 '18 22:03 j

Can you elaborate please, give me some theoretical example code?

NoNameProvided avatar Mar 22 '18 22:03 NoNameProvided

Here's how Symfony does it in the PHP world:

https://symfony.com/doc/current/reference/constraints/UniqueEntity.html

j avatar Mar 22 '18 22:03 j

In the class-validator world:

@IsUnique(WidgetRepository, ["name", "tag"])
export class Widget {
  @IsString()
  name: string;

  @IsString()
  @IsLowercase()
  tag: string;
}

@IsUnique would use something like typedi to get the repository from the container, and the array would be the fields that are to be unique.

I could think of a few more examples that would be nice to compare all fields at once. The main example is to do a single query utilizing the object after all field validations passed.

j avatar Mar 22 '18 22:03 j

Have you seen TypeORM? It's exactly what you are looking for.

NoNameProvided avatar Mar 22 '18 22:03 NoNameProvided

I'm not using TypeORM. I'm using MongoDB at the moment and they are pretty far behind in driver support. And not a lot of movement in MongoDB within TypeORM (issues grow, nothing getting done).

I'd like to push it to a validation library. The Symfony library I referenced above is a class validation library such as this one with support for validating DoctrineORM objects.

Edit: I'm also using https://github.com/19majkel94/type-graphql which has built-in support for this library.

j avatar Mar 22 '18 22:03 j

Then you can create such functionality here via

NoNameProvided avatar Mar 22 '18 22:03 NoNameProvided

@NoNameProvided I created a decorator already, but it's bound to the field and not the class, so I lack access to multiple fields.

Edit: I might be able to access multiple fields, but if so, it wouldn't be clear what the validation is trying to do.

j avatar Mar 22 '18 22:03 j

I created a decorator already, but it's bound to the field and not the class, so I lack access to multiple fields.

You can create multi-field validations. Here is an example: https://github.com/typestack/class-validator/issues/145#issuecomment-373949845

I might be able to access multiple fields, but if so, it wouldn't be clear what the validation is trying to do.

In your example, it totally makes sense to put it on the unique property instead of the class.

NoNameProvided avatar Mar 23 '18 00:03 NoNameProvided

Yes, in my fake example use-case, perhaps it can be solved "cleanishly" (although the two fields are not related at all, so IMO it's not clean and is actually more confusing).

Another example where it makes more sense on a class level:

@IsValidLocation(['address1', 'address2', 'city', 'state', 'zip'])
export class User {
  address1: string;
  address2: string;
  city: string;
  state: string;
  zip: string;
}

or

@IsValidLogin()
export class Auth {
  username: string;
  password: string;
}
// This may do a true lookup on the image, verify image size, etc, etc... I don't see this being clean on a field level.

@IsImageValid(({ baseUrl, name, extension }) => `${baseUrl)${name}${extension}`)
export class Image {
  @IsURL()
  baseURL: string;

  @IsString()
  name: string;

  @IsValidExtension()
  extension: string;
}

Others do it:

  • https://docs.jboss.org/hibernate/validator/4.1/reference/en-US/html/validator-usingvalidator.html#example-class-level
  • https://symfony.com/doc/current/reference/constraints/UniqueEntity.html

I see where if you're comparing a field to another field, then field level validation works and makes sense, but if the fields aren't being compared and are being used as a final validation constraint, then it'd make more sense on the class level. It can also be an area where you may do heavier validations. You can run your simple field level validations first, then do things like DB lookups, external requests, etc, on a class level so that simple validations can fail first.

As of now, I'd never do any of these examples with class-validator and am forced to do these types of validations hardcoded in my app. It'd be awesome to have it all in one place.

j avatar Mar 23 '18 18:03 j

This makes a lot of sense to me - I was thinking of exactly the same thing (specifically wanting to do Joi's with and or validators.

tonyxiao avatar Mar 29 '18 01:03 tonyxiao

Anything on this issue? Class level validators are very useful.

zveljkovic avatar Sep 03 '18 10:09 zveljkovic

Up, for example I have this case:

// Having at least one of the two set.
@ClassLevelValidator(post => post.articleId != null || post.externalLink != null)
export class PostDto {
  @IsNotEmpty()
  @IsString()
  title: string;

  @IsDefined()
  @IsString()
  shortDescription: string;

  @IsOptional()
  @IsInt()
  articleId: number; // if set I know this post relate to an article entity, that I can retrieve using the id

  @IsOptional()
  @IsUrl()
  externalLink: string; // if set I know this post relate to an external link.
}

// if both are set or none are set then this is a wrong entry.

It do exist in Java https://stackoverflow.com/questions/2781771/how-can-i-validate-two-or-more-fields-in-combination

ambroiseRabier avatar Apr 11 '19 15:04 ambroiseRabier

Generic Unique Constrain For any Column I hope we can Use below code like

File Name:- isUnique.ts @ValidatorConstraint({ async: true }) export class IsUniqueConstraint implements ValidatorConstraintInterface { validate(columnNameValue: any, args: ValidationArguments) { let columnNameKey = args.property; let tableName = args.targetName; return getManager().query("SELECT * FROM " + tableName + " WHERE " + columnNameKey + " = ?", [columnNameValue]).then(user => { if (user[0]) return false; return true; }); } } export function IsUnique(validationOptions?: ValidationOptions) { return function (object: Object, propertyName: string) { registerDecorator({ target: object.constructor, propertyName: propertyName, options: validationOptions, constraints: [], validator: IsUniqueConstraint }); }; }

Can be use in Typeorm Model Users as

@Column() @IsEmail({}, { message: 'Invalid email Address.' }) @IsUnique({ message: "Email $value already exists. Choose another name." }) email: string;

@Column() @Length(4, 20) @IsUnique({ message: "User Name $value already exists. Choose another name." }) username: string;

So it will be common for any model table.

ajaygangarde avatar Oct 02 '19 07:10 ajaygangarde

I was also hoping for a class-level validator, any news about this?

fullofcaffeine avatar Nov 14 '19 00:11 fullofcaffeine

Voting up for class level decorators. Would be a useful feature.

LendaVadym avatar Apr 02 '20 19:04 LendaVadym

@NoNameProvided this is pretty wanted.

j avatar Apr 06 '20 16:04 j

Sure. We are open to proposals how this may works. But for now this is future request.

vlapo avatar Apr 07 '20 20:04 vlapo

Hey, I made a tiny package that solves this exact problem, I tried to remain as close as possible to class-validator API https://github.com/astahmer/entity-validator/

astahmer avatar Apr 20 '20 19:04 astahmer

my need for "class level" is to validate across multiple properties at the same time versus just one... i was concerned when i saw this issue still open... yet issue #759 referencing the docs Custom validation decorators - @IsLongerThan() example shows how to reference the any of the current objects properties inside the validator so i'm hopeful i can adapt that to my needs... just wanted to post in case it helps others

Beej126 avatar Oct 29 '20 04:10 Beej126

Any updates on this issue?

royi-frontegg avatar Dec 11 '20 15:12 royi-frontegg

I have created a workaround for this issue, but I don't know if it is a good approach.

It works by creating a property named _classValidationProperty on target prototype and registering a property decorator to it. Inside this property decorator, you have access to object been validated, so you can do any validations using multiple properties of the object. I also created a helper function to create the decorators.

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

@ValidateFoo()
export class Foo {
  a: number;
  b: number;
  c: number;
}

interface Class {
  new(...args: any[]): {};
}

export function ValidateFoo() {
  return createClassValidationDecorator('Foo', (value: any, args: ValidationArguments) => {
    // Do whatever validation you need
    const foo = args.object as Foo;
    return (foo.a === foo.b) && (foo.a === foo.c);
  });
}

// Another Example
export function ValidateBar() {
  // validateBarFunction is defined somewhere else
  return createClassValidationDecorator('bar', validateBarFunction);
}

export function createClassValidationDecorator(validatorName: string, validateFunc: (value: any, args: ValidationArguments) => any) {
  return function decorateClass<T extends Class>(target: T) {
    console.log('>>> Decorating class:', target.name);
    let _classValidatorRegistered = false;

    return class extends target {
      private readonly _classValidationProperty;

      constructor(...args: any[]) {
        super(...args);
        console.log(`Executing ${target.name}.constructor`);
        if (!_classValidatorRegistered) {
          _classValidatorRegistered = true;
          console.log(`Registering property validator for ${target.name}._classValidationProperty`);
          registerDecorator({
            name: validatorName,
            target: this.constructor,
            propertyName: '_classValidationProperty',
            // constraints: ['...'],
            // options: validationOptions,
            validator: {
              validate: validateFunc
            },
          });
        }
      }
    };
  }
}

ModestinoAndre avatar Jan 21 '21 15:01 ModestinoAndre

up!

lucasltv avatar Mar 23 '21 03:03 lucasltv

That was very useful @ModestinoAndre, thanks.

I ended up with a similar solution quite close to yours if anyone needs it some day, which works with Nestjs InputType / ObjectType decorators as well:

import {
  registerDecorator,
  ValidatorConstraintInterface,
} from 'class-validator';
import { MarkRequired } from 'ts-essentials';

type AnyClass = { new (...args: any[]): any };

type PublicConstructor = new (...args: any[]) => any;

/** The response type for registerClassValidator() */
export type ClassValidationDecorator = <T extends AnyClass>(
  target: T,
) => PublicConstructor;

/** A helper method to create a new Class-level validation decorator. */
export function registerClassValidator(options: {
  name: string;
  validator: new () => MarkRequired<
    ValidatorConstraintInterface,
    'defaultMessage'
  >;
  constraints: any[];
}): ClassValidationDecorator {
  return function decorateClass<T extends AnyClass>(
    target: T,
  ): PublicConstructor {
    const { name, validator, constraints } = options;

    registerDecorator({
      name,
      target,
      propertyName: target.name,
      constraints,
      validator,
      options: {
        always: true,
      },
    });

    return target;
  };
}

And then I can use it like this:

/** The class-validator constraint for the RequiredTogether() class decorator */
@ValidatorConstraint()
class RequiredTogetherConstraint implements ValidatorConstraintInterface {
  validate(value: undefined, args: ValidationArguments): boolean {
    const [requiredFields] = args.constraints;

    return (
      requiredFields.every((field: string) => field in args.object) ||
      requiredFields.every((field: string) => !(field in args.object))
    );
  }

  defaultMessage(args: ValidationArguments): string {
    const [fields] = args.constraints;

    return 'All fields must exist together or not at all: ' + fields.join(', ');
  }
}

/**
 * A class level decorator to check that all or none of the fields exist.
 * This is useful when using optional fields but must all exist together.
 */
export function RequiredTogether(fields: string[]): ClassValidationDecorator {
  return registerClassValidator({
    name: 'RequiredTogether',
    validator: RequiredTogetherConstraint,
    constraints: [fields],
  });
}

DustinJSilk avatar Jun 24 '21 13:06 DustinJSilk

Need this feature too!!

Char2sGu avatar Jun 26 '21 04:06 Char2sGu

Marshmallow (a schema validator for Python) also has a feature like this: @validates_schema.

Some use-cases where this is useful:

  • you want some fields to be mutually exclusive (only 1 field of 3 can be defined)
  • you want to ensure that a startDate field comes before an endDate field
  • you want to ensure various combinations of fields come together to form something semantically meaningful (various combinations of address fields actually create an address)
  • you want to do some validation in your API layer because you're storing raw JSON in a database and don't want to push that validation into a trigger

juliusiv avatar Jul 23 '21 18:07 juliusiv

any updates about this?

hanchchch avatar May 03 '23 01:05 hanchchch

Hello any news on this please?

slukes avatar Feb 27 '24 18:02 slukes