typestack / class-validator

Decorator-based property validation for classes.
MIT License
11.07k stars 803 forks source link

How to customize validation messages globally? #169

Open chanlito opened 6 years ago

chanlito commented 6 years ago

Instead of passing custom message to each decorator.

kanatkubash commented 6 years ago

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);
}
NoNameProvided commented 6 years ago

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

MichalLytek commented 6 years ago

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) 😉

mendrik commented 6 years ago

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

bruno-lombardi commented 5 years ago

@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.

josephbuchma commented 5 years ago

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

adamscybot commented 4 years ago

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

borjadev commented 4 years ago

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

tmtron commented 4 years ago

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.

ChrisKatsaras commented 4 years ago

@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 😄

tmtron commented 4 years ago

@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...

ChrisKatsaras commented 4 years ago

@tmtron fair enough! Thanks for clarifying

behruzz commented 4 years ago

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

tmtron commented 4 years ago

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.

oney commented 4 years ago

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.

lazarljubenovic commented 4 years ago

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.

jbjhjm commented 4 years ago

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

tiamo commented 4 years ago

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",
}
EndyKaufman commented 4 years ago

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
});
Beej126 commented 3 years ago

sharing some agreeably terse message override examples, compatible with latest v0.13.1:

import {
    ValidationOptions, buildMessage, ValidateBy,
    IsNotEmpty as _IsNotEmpty,
    MaxLength as _MaxLength,
    Min as _Min,
    Max as _Max
} from "class-validator";

//lookup existing message interpolation patterns in the source:
//https://github.com/typestack/class-validator/blob/develop/src/decorator/number/Max.ts

export const IsNotEmpty = (validationOptions?: ValidationOptions): PropertyDecorator =>_IsNotEmpty({...validationOptions, message: "Required"});
export const MaxLength = (max: number, validationOptions?: ValidationOptions): PropertyDecorator =>_MaxLength(max, {...validationOptions, message: "$constraint1 chars max" });
export const Min = (minValue: number, validationOptions?: ValidationOptions): PropertyDecorator =>_Min(minValue, {...validationOptions, message: ">= $constraint1"});
export const Max = (maxValue: number, validationOptions?: ValidationOptions): PropertyDecorator =>_Max(maxValue, {...validationOptions, message: `$constraint1 max`});
rafaelbrier commented 3 years ago

Seriously, this functionality hasn't still gone out? I wonder why nestJs chooses this library...

icedcrow commented 3 years ago

Well, maybe a tricky way:

// OverrideOptions.ts
import { ValidationOptions, getMetadataStorage } from 'class-validator';
import type { ValidationMetadata } from 'class-validator/types/metadata/ValidationMetadata';

const OverrideOptions = (options: ValidationOptions): PropertyDecorator => {
  return (object, propertyName) => {
    const storage = getMetadataStorage();
    const metadataList: ValidationMetadata[] = Reflect.get(
      storage,
      'validationMetadatas',
    );
    (metadataList || [])
      .filter((v) => v.propertyName === propertyName)
      .forEach((v) => {
        Object.assign(v, options);
      });
  };
};

export default OverrideOptions;

usage:

import { Type } from 'class-transformer';
import { MinLength, MaxLength, IsNotEmpty, validate } from 'class-validator';
import OverrideOptions from './OverrideOptions';

class TestEntity {

  @OverrideOptions({ message: 'use this message' })
  @MaxLength(20)
  @MinLength(10)
  @IsNotEmpty()
  @Type(() => String)
  readonly testName!: string;

}

const entity = new TestEntity();
entity.name = 'test';
validate(entity, { stopAtFirstError: true })
  .then(() => {
    // ...
  })
Ami777 commented 3 years ago

Based on @kkoomen solution (https://github.com/ToonvanStrijp/nestjs-i18n/issues/97#issuecomment-826336477) I've modified it to be able to translate errors on front-end + to give me field name where the error should be visible.

The code

import {
  UnprocessableEntityException,
  ValidationError,
  ValidationPipe,
  ValidationPipeOptions,
} from '@nestjs/common';

const classValidationPatterns = [
  '$IS_INSTANCE decorator expects and object as value, but got falsy value.',
  '$property is not a valid decimal number.',
  '$property must be a BIC or SWIFT code',
  '$property must be a boolean string',
  '$property must be a boolean value',
  '$property must be a BTC address',
  '$property must be a credit card',
  '$property must be a currency',
  '$property must be a data uri format',
  '$property must be a Date instance',
  '$property must be a Firebase Push Id',
  '$property must be a hash of type (.+)',
  '$property must be a hexadecimal color',
  '$property must be a hexadecimal number',
  '$property must be a HSL color',
  '$property must be a identity card number',
  '$property must be a ISSN',
  '$property must be a json string',
  '$property must be a jwt string',
  '$property must be a latitude string or number',
  '$property must be a latitude,longitude string',
  '$property must be a longitude string or number',
  '$property must be a lowercase string',
  '$property must be a MAC Address',
  '$property must be a mongodb id',
  '$property must be a negative number',
  '$property must be a non-empty object',
  '$property must be a number conforming to the specified constraints',
  '$property must be a number string',
  '$property must be a phone number',
  '$property must be a port',
  '$property must be a positive number',
  '$property must be a postal code',
  '$property must be a Semantic Versioning Specification',
  '$property must be a string',
  '$property must be a valid domain name',
  '$property must be a valid enum value',
  '$property must be a valid ISO 8601 date string',
  '$property must be a valid ISO31661 Alpha2 code',
  '$property must be a valid ISO31661 Alpha3 code',
  '$property must be a valid phone number',
  '$property must be a valid representation of military time in the format HH:MM',
  '$property must be an array',
  '$property must be an EAN (European Article Number)',
  '$property must be an email',
  '$property must be an Ethereum address',
  '$property must be an IBAN',
  '$property must be an instance of (.+)',
  '$property must be an integer number',
  '$property must be an ip address',
  '$property must be an ISBN',
  '$property must be an ISIN (stock/security identifier)',
  '$property must be an ISRC',
  '$property must be an object',
  '$property must be an URL address',
  '$property must be an UUID',
  '$property must be base32 encoded',
  '$property must be base64 encoded',
  '$property must be divisible by (.+)',
  '$property must be empty',
  '$property must be equal to (.+)',
  '$property must be locale',
  '$property must be longer than or equal to (\\S+) and shorter than or equal to (\\S+) characters',
  '$property must be longer than or equal to (\\S+) characters',
  '$property must be magnet uri format',
  '$property must be MIME type format',
  '$property must be one of the following values: (\\S+)',
  '$property must be RFC 3339 date',
  '$property must be RGB color',
  '$property must be shorter than or equal to (\\S+) characters',
  '$property must be shorter than or equal to (\\S+) characters',
  '$property must be uppercase',
  '$property must be valid octal number',
  '$property must be valid passport number',
  '$property must contain (\\S+) values',
  '$property must contain a (\\S+) string',
  '$property must contain a full-width and half-width characters',
  '$property must contain a full-width characters',
  '$property must contain a half-width characters',
  '$property must contain any surrogate pairs chars',
  '$property must contain at least (\\S+) elements',
  '$property must contain not more than (\\S+) elements',
  '$property must contain one or more multibyte chars',
  '$property must contain only ASCII characters',
  '$property must contain only letters (a-zA-Z)',
  '$property must contain only letters and numbers',
  '$property must match (\\S+) regular expression',
  '$property must not be greater than (.+)',
  '$property must not be less than (.+)',
  '$property should not be empty',
  '$property should not be equal to (.+)',
  '$property should not be null or undefined',
  '$property should not be one of the following values: (.+)',
  '$property should not contain (\\S+) values',
  '$property should not contain a (\\S+) string',
  "$property's byte length must fall into \\((\\S+), (\\S+)\\) range",
  "All $property's elements must be unique",
  'each value in ',
  'maximal allowed date for $property is (.+)',
  'minimal allowed date for $property is (.+)',
  'nested property $property must be either object or array',
];

/**
 * The class-validator package does not support i18n and thus we will
 * translate the error messages ourselves.
 */
function translateErrors(validationErrors: ValidationError[]) {
  const errors = validationErrors.map((error: ValidationError): string[] => Object.keys(error.constraints).map((key: string): any => { /*
  Real type:
  {
    field: string;
    txt: string;
    params: {
      [key: string]: any;
    };
  }
  */
    let match: string[] | null;
    let constraint: string;

    // Find the matching pattern.
    for (const validationPattern of classValidationPatterns) {
      const pattern = validationPattern.replace('$', '\\$');
      constraint = error.constraints[key].replace(error.property, '$property');
      match = new RegExp(pattern, 'g').exec(constraint);
      if (match) {
        break;
      }
    }

    // Replace the constraints values back to the $constraintX words.
    let i18nKey = constraint;
    const replacements = { property: error.property };
    if (match) {
      for (let i = 1; i < match.length; i += 1) {
        i18nKey = i18nKey.replace(match[i], `{{constraint${i}}}`);
        replacements[`constraint${i}`] = match[i];
      }
    }

    // Get the i18n text.
    return {
      field: error.property,
      txt: i18nKey.replace('$property', '{{property}}'),
      params: replacements,
    };
  }));

  const errorsFlattened = errors.reduce((data: string[], errors) => {
    data.push(...errors);
    return data;
  }, []);

  return new UnprocessableEntityException(errorsFlattened);
}

export const createValidationPipeWithI18n = (options: ValidationPipeOptions) => new ValidationPipe({
  ...options,
  exceptionFactory: translateErrors,
});

And the demo image

mehmetsaitdas commented 3 years ago

validation.pipe.ts

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';

import { resolve } from 'path';
import { readFileSync } from 'fs';
import { plainToClass } from 'class-transformer';
import { validate } from "class-validator-multi-lang";
const EN_I18N_MESSAGES = JSON.parse(readFileSync(resolve(__dirname, '../../node_modules/class-validator-multi-lang/i18n/en.json')).toString());
const TR_I18N_MESSAGES = JSON.parse(readFileSync(resolve(__dirname, '../../node_modules/class-validator-multi-lang/i18n/tr.json')).toString());

@Injectable()
export class ValidationPipe implements PipeTransform<any> {

    strLang: String = "en";

    async transform(value: any, { metatype }: ArgumentMetadata) {

        value == "en" ? this.strLang = "en" : value == "tr" ? this.strLang = "tr" : "";

        if (!metatype || !this.toValidate(metatype)) {
            return value;
        }
        const object = plainToClass(metatype, value);
        const errors = await validate(object, { messages: this.strLang == "tr" ? TR_I18N_MESSAGES : EN_I18N_MESSAGES });
        if (errors.length > 0) {
            throw new BadRequestException(`${this.formatErrors(errors)}`);
        }
        return value;
    }

    private toValidate(metatype: Function): boolean {
        const types: Function[] = [String, Boolean, Number, Array, Object];
        return !types.includes(metatype);
    }

    private formatErrors(errors: any[]) {
        return errors.map(err => {
            for (let property in err.constraints) {
                return err.constraints[property];
            }
        }).join(',')
    }
}

222222

Iran-110 commented 3 years ago

I think it's better to do internationalization at the top level, means by calling validate function or validateSync, at first, all processes must be done and after that, internationalization in the last line of the function must be performed.

But, the settings should be flexible as much as possible, e.g. by overriding in the class as a new decorator or as an extra argument to the existing decorators.

  1. validate function, the validate function can get a new option like i18n with this shape:

    validate(object, {
    i18n: {
      lang: "en", // it's important to handle each HTTP request separately
    
      // The description of this option is at the end.
      replaceValueByClassValidation: {
         property: false,
         value: true,
         target: true,
         constraints: true
      },
    
      // It seems a customizable transform function is needed. (e.g. this can be use to convert some property to count which i18n supports)
      transformBeforeTranslation: (template: string) => template.replace(/\$\w+/, /* a replace function */}),
    
      // this is the translator function
      translator: (constraintKey, variables, configs) => {
         i18next.t(constraintKey, {...variables, interpolation: configs});
      }
    }
    })
  2. decorator point of view: The most important thing to consider is that each decorator function (such as Min, Max, etc.) must have a special instance of i18n object to override. Besides, they should access to the central dictionary of key-templates of other languages.

class Class {
   @Max(100, {i18n: {/* overridden options */}})
   @Min(0)
   mathScore: number;
}
  1. new decoration like I18n: It's a good idea to override the validate settings with a new default value for a batch of decorators using a bottom-level new decorator:
    class Class {
    @Max(100, {i18n: {/* per decorator overridden options */}})
    @Min(0)
    @I18n({lang: 'fa', /* by calling this at the bottom-level, this option only overrides the validation options, but does not override the Min and Max values */})
    property: number;
    }

A description that why replaceByClassValidation is needed: Consider this settings:

import {validate} from "class-validator";

class Class {
   @Max(100, {i18n: {/* overridden options */}})
   @Min(0)
   mathScore: number;
}
const object = new Class();
object.mathScore = 102;

validate(object, {lang: 'fa-IR'}).catch(errors => { console.dir(errors) });

The output is like this:

{
    target: /* object */;
    property: 'mathScore';
    value: 102;
    constraints: {
        // $property must not be greater than $constraint1
        // English version
        'Max': "mathScore must not be greater than 100",

        // Persian version
        'Max': "mathScore نباید بیشتر از 100 باشد.",
    };
    children: [];
}

But we don't want to use mathScore in our languages. Because, sometimes, we want to render something in client-side. As a result, we can use replaceByClassValidation:

validate(object, {lang: 'fa-IR', replaceByClassValidation: {property: false}}).catch(errors => {
    console.dir(errors);
});

So, we have:

{
    target: /* object */;
    property: 'mathScore';
    value: 102;
    constraints: {
        // $property must not be greater than $constraint1
        // English version
        'Max': "$property must not be greater than 100",

        // Persian version
        'Max': "$property نباید بیشتر از 100 باشد.",
    };
    children: [];
}

and we can replace it in the client side.

averri commented 3 years ago

I think the support for internationalization is extremelly important. I would like to implement the translations on the client side. What I want is to receive the i18n key and the associated data and let the client side do the translations.

For example, for a property called username annotated with @Length(3, 20), when the validation fails, I want to receive the following data:

{
  "statusCode": 400,
  "message": [
    {"username": "validation.length", "data": {"min": 3, "max": 20}}
  ],
  "error": "Bad Request"
}

This format is very useful for advanced and professional translations (see https://formatjs.io/).

Is there any workaround to support the above message format?

averri commented 3 years ago

sharing some agreeably terse message override examples, compatible with latest v0.13.1:

import {
    ValidationOptions, buildMessage, ValidateBy,
    IsNotEmpty as _IsNotEmpty,
    MaxLength as _MaxLength,
    Min as _Min,
    Max as _Max
} from "class-validator";

//lookup existing message interpolation patterns in the source:
//https://github.com/typestack/class-validator/blob/develop/src/decorator/number/Max.ts

export const IsNotEmpty = (validationOptions?: ValidationOptions): PropertyDecorator =>_IsNotEmpty({...validationOptions, message: "Required"});
export const MaxLength = (max: number, validationOptions?: ValidationOptions): PropertyDecorator =>_MaxLength(max, {...validationOptions, message: "$constraint1 chars max" });
export const Min = (minValue: number, validationOptions?: ValidationOptions): PropertyDecorator =>_Min(minValue, {...validationOptions, message: ">= $constraint1"});
export const Max = (maxValue: number, validationOptions?: ValidationOptions): PropertyDecorator =>_Max(maxValue, {...validationOptions, message: `$constraint1 max`});

Thank you so much for this solution, it's really concise and works great.

averri commented 3 years ago

My workaround, based on @Beej126 solution:

export const IsOptional = _IsOptional;

export const Validate = _Validate;

function toJson(key: string, args: ValidationArguments, data?: any): string {
  return JSON.stringify({key, field: args.property, data});
}

export function intlMsg(key: string, data?: any) {
  return (args: ValidationArguments) => toJson(key, args, data);
}

export const IsNotEmpty = (opts?: ValidationOptions): PropertyDecorator =>
  _IsNotEmpty({...opts, message: intlMsg('validation.isNotEmpty')});

export const IsDate = (opts?: ValidationOptions): PropertyDecorator =>
  _IsDate({...opts, message: intlMsg('validation.isDate')});

export const IsIn = (values: readonly any[], opts?: ValidationOptions): PropertyDecorator =>
  _IsIn(values, {...opts, message: intlMsg('validation.isIn', {values})});

export const IsEmail = (eOpts?: ValidatorJS.IsEmailOptions, opts?: ValidationOptions): PropertyDecorator =>
  _IsEmail(eOpts, {...opts, message: intlMsg('validation.isEmail')});

export const Length = (min: number, max: number, opts?: ValidationOptions): PropertyDecorator =>
  _Length(min, max, {...opts, message: intlMsg('validation.length', {min, max})});

export const MaxLength = (max: number, opts?: ValidationOptions): PropertyDecorator =>
  _MaxLength(max, {...opts, message: intlMsg('validation.maxLength', {max})});

export const IsBoolean = (opts?: ValidationOptions): PropertyDecorator =>
  _IsBoolean({...opts, message: intlMsg('validation.isBoolean')});

export const MaxDate = (date: Date, opts?: ValidationOptions): PropertyDecorator =>
  _MaxDate(date, {...opts, message: intlMsg('validation.maxDate', {date})});

export const IsPastDate = (opts?: ValidationOptions): PropertyDecorator =>
  _MaxDate(new Date(), {...opts, message: intlMsg('validation.isPastDate')});

I have created an exeption filter to convert the JSON string to JSON object before sending the error back to the client. So this is what I get now (cool!):

{
  "status": 400,
  "error": "Bad Request",
  "messages": [
    {
      "key": "validation.length",
      "field": "username",
      "data": {
        "min": 3,
        "max": 20
      }
    },
    {
      "key": "validation.isPastDate",
      "field": "birthDate"
    },
    {
      "key": "validation.isValidLocale",
      "field": "locale"
    }
  ]
}

The UI can feed the key and data to the i18n functions. Example of i18n message templates using the ICU syntax:

image

juni0r commented 2 years ago

"Hey, I would like to localize my validation error messages."

Every other validation library in existence: "Well, of course. Here you go!" class-validator: ¯\(ツ)/¯

netojose commented 2 years ago

Yes @juni0r , and this issue is opened since 2018, and until now... nothing :slightly_frowning_face:

pmirand6 commented 2 years ago

I came from Laravel, and now I'm using NestJS. I can't believe that this stills an issue

EndyKaufman commented 2 years ago

I came from Laravel, and now I'm using NestJS. I can't believe that this stills an issue

NestJS module for adding translations to the application, with a pipe for translating validation errors

https://dev.to/endykaufman/nestjs-module-for-adding-translations-to-the-application-with-a-pipe-for-translating-validation-errors-2mf3

pmirand6 commented 2 years ago

I came from Laravel, and now I'm using NestJS. I can't believe that this stills an issue

NestJS module for adding translations to the application, with a pipe for translating validation errors

https://dev.to/endykaufman/nestjs-module-for-adding-translations-to-the-application-with-a-pipe-for-translating-validation-errors-2mf3

Thanks @EndyKaufman for sharing, but I cant figured out how to implement a new message validation file and also, this would map a custom validation message?

EndyKaufman commented 2 years ago

https://github.com/EndyKaufman/class-validator-multi-lang/blob/i18n/test/functional/i18n.spec.ts#L294 image

EndyKaufman commented 2 years ago

https://github.com/EndyKaufman/class-validator-multi-lang/blob/i18n/i18n/en.json

pmirand6 commented 2 years ago

https://github.com/EndyKaufman/class-validator-multi-lang/blob/i18n/i18n/en.json

Oh wow - so for my case, I need to put the json file in the node module folder and in that case I need to also add all spanish messages?

EndyKaufman commented 2 years ago

image

pmirand6 commented 2 years ago

image

Perfect @EndyKaufman thanks!

EndyKaufman commented 2 years ago

@pmirand6 you may use exists Spanish locale https://github.com/EndyKaufman/class-validator-multi-lang/blob/i18n/i18n/es.json, if you found errors you may add correct translate in https://crowdin.com/project/class-validator

john-wennstrom commented 2 years ago

In Nestjs you can bootstrap the app with useGlobalPipes() and then set options for the class-validator globally. So I wanted the api to return validation errors in this format:

{
    "statusCode": 400,
    "message": [
        {
            "legalName": {
                "isNotEmpty": "legalName should not be empty"
            }
        },
        {
            "organizationNo": {
                "isNotUnique": "organizationNo is not unique"
            }
        },
        {
            "email": {
                "isEmail": "email must be an email"
            }
        }
    ],
    "error": "Bad Request"
}

What I had to do was to use the exceptionFactory option like this:

async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    app.enableCors();
    app.useGlobalPipes(
        new ValidationPipe({
            exceptionFactory: (errors) => {
                const result = errors.map((error) => ({
                    [error.property]: error.constraints,
                }));
                return new BadRequestException(result);
            },
        }),
    );
    await app.listen(3000);
}
bootstrap();
netojose commented 2 years ago

This is a good idea, @john-wennstrom , but this doesn't allows for example, set errors messages with different languages and/or use user's language to display errors in a user language.

pedrosodre commented 2 years ago

To solve this in NestJs I made something similar with @john-wennstrom's solution, but I've added an exception filter in my implementation as well.

I created a validation.exception.ts file, a simple class to work as a custom exception.

import { ValidationError } from '@nestjs/common';

export class ValidationException {
    constructor(public errors: ValidationError[]) {}
}

Then I used the exceptionFactory from ValidationPipe, like this:

    app.useGlobalPipes(
        new ValidationPipe({`
            exceptionFactory: (errors) => new ValidationException(errors),
            // ...
        }),
    );

I've also used @Beej126's solution as well to allow i18n to see validation parameters, like this:

import {
    MinLength as _MinLength,
    IsIn as _IsIn,
    ValidationOptions,
} from 'class-validator';

export const MinLength = (
    min: number,
    validationOptions?: ValidationOptions,
): PropertyDecorator =>
    _MinLength(min, {
        ...validationOptions,
        context: { min },
        // if you need to use context as well, you can do something like that: context: { ...(validationOptions.context || {}), min },
    });

Allowing me to translate MinLength error like this on i18n json file:

{
    "minLength": "must be longer than or equal to {min} characters"
}

And finally I made a custom exception filter (in my case called i18n-validation-exception.filter.ts), like this:

import {
    ExceptionFilter,
    Catch,
    ArgumentsHost,
    BadRequestException,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { getI18nContextFromRequest } from 'nestjs-i18n';
import { ValidationException } from './validation.exception';

@Catch(ValidationException)
export class I18nValidationExceptionFilter implements ExceptionFilter {
    private readonly i18nFile = 'validation';

    catch(exception: ValidationException, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<Response>();
        const request = ctx.getRequest<Request>();
        const baseException = new BadRequestException(); // You don't need to use BadRequestException here as reference, but I preferred to do it like this

        const i18n = getI18nContextFromRequest(request);

        response.status(baseException.getStatus()).json({
            statusCode: baseException.getStatus(),
            message: baseException.getResponse(),
            errors: exception.errors.map((error) => {
                return {
                    property: error.property,
                    children: error.children,
                    constraints: Object.keys(error.constraints).map((constraint) =>
                        i18n.t(`${this.i18nFile}.${constraint}`, {
                            defaultValue: error.constraints[constraint],
                            args: {
                                ...error,
                                ...(error.contexts?.[constraint] || {}),
                            },
                        }),
                    ),
                };
            }),
        });
    }
}

Hope it helps you as well, @netojose :)

netojose commented 2 years ago

Thanks, @pedrosodre :)

buddh4 commented 2 years ago

I've described two possible solutions here https://github.com/typestack/class-validator/issues/1758 which would both do not require major changes and would enable custom translations. I'd really love to have a solution for this issue since I do not want to wrap all available decorators only to revert all of my models once a solution is available...

olawalejuwonm commented 1 year ago

Hello, Please i just want to define my error messages globally for each decorator is that possible now?

netojose commented 1 year ago

@olawalejuwonm , I guess is not possible. It's crazy how a simple feature is not possible.

olawalejuwonm commented 1 year ago

@netojose is pr accepted for this?

picardplaisimond commented 1 year ago

I can't believe this is still an issue today...

netojose commented 1 year ago

@netojose is pr accepted for this?

Not yet. This issue is opened in 2018, and until now, nothing. On this issue, there are described some work arounds, maybe one of them works for you.

mikila85 commented 1 year ago

I just get "email must be an email" and want to change it to "email is not valid" :( WHAT IS SO HARD ABOUT THIS REQUEST??? 5 years LMFAO... @vlapo @Cyri-L @DystopianProgrammer @Kiliandeca @henrikra @edcarroll @christophercr @MichalLytek