Class-validator: How to customize validation messages globally?

Created on 6 Feb 2018  Â·  19Comments  Â·  Source: typestack/class-validator

Instead of passing custom message to each decorator.

feature

Most helpful comment

It would be really nice and allow for i18n of the error messages. Really useful while reusing validation classes on backend (english) and on frontend (multilanguage) 😉

All 19 comments

Maybe you could write your own function that is similar to Validator Functions definition . Then in that custom function set message and call corresponding validation function from class-validator.
Something like

var CustomMatches = (pattern: RegExp, validationOptions?: ValidationOptions) => {
    validationOptions = {};
    validationOptions.message = "Message";
    return Matches(pattern, validationOptions);
}

Hey, this is on my list, I will add it.

It would be really nice and allow for i18n of the error messages. Really useful while reusing validation classes on backend (english) and on frontend (multilanguage) 😉

@chanlito as work around I do it by delegating new decorators to existing ones:
export const IsRequired: Function = () => IsNotEmpty({ message: () => translate('validation.required') });

@mendrik how would you create that translate decorator? Which library do you use? I see that there is nestjs-i18n, but it does not provide you that kind of validator decorator.

@bruno-lombardi translate is not a decorator in example above, it's just a regular function that returns translated string by given key.

Trouvble is I want all the constraints back not just key code. You cant get the contraints in the error for rendering later.

Any planning on the road map to add this feature? I would like to integrate i18next (i18next-express-middleware) with class-validator, but I don't know how to do this

NOTE: this only works in versions < 0.12!

As a workaround, we monkey patch ValidationTypes.getMessage():

export function patchClassValidatorI18n() {
  const orig = ValidationTypes.getMessage.bind(ValidationTypes);
  ValidationTypes.getMessage = (type: string, isEach: boolean): string | ((args: ValidationArguments) => string) => {
    switch (type) {
      case ValidationTypes.IS_NOT_EMPTY:
        return i18next.t('msg.inputRequired');
      case ValidationTypes.MAX_LENGTH:
        return i18next.t('validation.inputLength.tooLong', {
          threshold: '$constraint1'
        });

      // return the original (English) message from class-validator when a type is not handled
      default:
        return orig(type, isEach);
    }
  };
}

Then call patchClassValidatorI18n at the start of your entry-point (e.g. in main.ts, test-setup.ts, ..).
We use i18next for translations, but you can simple replace i18next.t with your own custom translation function.

@tmtron the only thing to note with your fix is that if the function name was to ever change in a future version, you would have to update your reference. Not a big deal for most but something to keep in mind for anyone looking to implement your solution 😄

@ChrisKatsaras since we use typescript, such a change will cause a compilation error.
In addition to that we have unit tests for each message. which would fail (in the unlikely case, that the typedefs are wrong): so there is nothing to worry about...

@tmtron fair enough! Thanks for clarifying

Hey, this is on my list, I will add it.

Hey, Did you add this? When are you planning to add? There is a PR from @HonoluluHenk

I think for translations on the backend (node.js) we need to pass some context (e.g. language/locale of the current user) to the message function.
e.g. ValidatorOptions should get an invocationContext and this context must be passed via the ValidationArguments to the message function
note, that this invocationContext is different than the existing ValidationArguments.context, because this can be different for each validate* call

use case: e.g. some automatic task on the backend which sends emails to the users - each user may have a different locale/language. We cannot simply set a global variable due to the async nature of nodejs.

On backend, it's important that i18n should NOT be handled in message function directly

class Post {
  @Length(10, 20, {
    message: (args: ValidationArguments) => i18n.t("LengthTranslation", args),
  })
  title!: string;

The above is wrong on backend because when you validate an object, it's undetermined that WHO will see the error message.

A better way is only doing the translation when you really know who will see the error messages. This means that you translate in controllers, GraphQL formatError, sending push notifications or even socket.io emit.

But one requirement to make translation easier is that the error should carry enough information to translate.

When you do validate(post).then((errors) => {, errors is ValidationError[]

export declare class ValidationError {
    target?: Object;
    property: string;
    value?: any;
    constraints?: {
        [type: string]: string;
    };
    children: ValidationError[];
    contexts?: {
        [type: string]: any;
    };
}

ValidationError actually carries many information for translation, but it still lacks for specific translation keys.

The solution is

class Post {
  @Length(10, 20, {
    context: {
      i18n: "LengthKey",
    },
  })
  title!: string;
}

validate(post).then((errors) => {
  const msgs = errors
    .map((e) => {
      const collect = [];
      for (const key in e.contexts) {
        collect.push(
          i18next.t(e.contexts[key].i18n, {
            target: e.target,
            property: e.property,
            value: e.value,
          })
        );
      }
      return collect;
    })
    .flat();

However, it's unfortunate that ValidationError's constraints is message strings, not args: ValidationArguments.

I am disappointed that defaultMessage has to return string as per the ValidatorConstraintInterface interface. Every API should return objects representing an error, not (only) human-readable sentences. length: { min: 1, max: 10 } is way more useful to developers over (only) "the length must be between 1 and 10".

_THEN_ this object should be used for a template syntax such as "the length must be between $min and $max". This library provides default messages, but doesn't allow changing the default message globally, which means it's stuck at the wording, casing and language that the developer has chosen.

Given that this is a validation library, error reporting should be its top priority. @NoNameProvided, can we get an update on this? The last word from team members is from 2018; would appreciate to know if I should expect this to be addressed soon.

Are there any news on this? My whole validation setup is stuck on old class-validator version because there's no solution to manage error messages. I saw that over here there already is a open pull request implementing a basic solution but nothing has happened since. https://github.com/typestack/class-validator/pull/238

My simple workaround

import { ValidationOptions } from 'class-validator';
import snakeCase from 'lodash/snakeCase';
import i18n from 'i18next';

export const ValidateBy = (
  validator: (...args: any[]) => PropertyDecorator,
  args: any[] = [],
): PropertyDecorator => {
  args.push(
    <ValidationOptions>
      {
        message: (validationArgs) => i18n.t(
          'validation:' + snakeCase(validator.name),
          validationArgs,
        ),
      },
  );
  return validator(...args);
};

then use

  @ValidateBy(IsNotEmpty)
  @ValidateBy(MinLength, [6])
  readonly password: string;

validation.json

{
  "is_not_empty": "{{property}} should not be empty",
  "min_length": "{{property}} must be longer than or equal to {{constraints.0}} characters",
}

i create PR basic support i18n: https://github.com/typestack/class-validator/pull/730

example usages:

import { IsOptional, Equals, Validator, I18N_MESSAGES } from 'class-validator';
class MyClass {
  @IsOptional()
  @Equals('test')
  title: string = 'bad_value';
}
Object.assign(I18N_MESSAGES, {
  '$property must be equal to $constraint1': '$property должно быть равно $constraint1',
});
const model = new MyClass();
validator.validate(model).then(errors => {
  console.log(errors[0].constraints);
  // out: title должно быть равно test
});
Was this page helpful?
0 / 5 - 0 ratings