-
Notifications
You must be signed in to change notification settings - Fork 811
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
How to customize validation messages globally? #169
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. 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: |
@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 |
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 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 |
@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. |
@tmtron fair enough! Thanks for clarifying |
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. 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 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 |
I am disappointed that THEN this object should be used for a template syntax such as 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. #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: #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
}); |
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`}); |
Perfect @EndyKaufman thanks! |
@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 |
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(); |
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. |
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 import { ValidationError } from '@nestjs/common';
export class ValidationException {
constructor(public errors: ValidationError[]) {}
} Then I used the 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": "must be longer than or equal to {min} characters"
} And finally I made a custom exception filter (in my case called 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 :) |
Thanks, @pedrosodre :) |
I've described two possible solutions here #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... |
Hello, Please i just want to define my error messages globally for each decorator is that possible now? |
@olawalejuwonm , I guess is not possible. It's crazy how a simple feature is not possible. |
@netojose is pr accepted for this? |
I can't believe this is still an issue today... |
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. |
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??? |
What's so hard about understanding that a project is dead? Certainly your all-caps rage ain't gonna bring it back. |
nice one @lazarljubenovic =) it's a quiet saturday afternoon where i'm at so why not pile on a little... @mikila85, @picardplaisimond, @olawalejuwonm, etc... we did the wrappers for custom messaging that i mentioned up in this thread over 2 years ago and those have been rock solid without need for change still cranking great for tons of production usage every day, making it very much not dead to us... the customizing is not hard code... if you look at this library as a nice core engine for managing typescript decorators and then customizing to your needs on top of that, it is very useful... even for adding in your own new validation routines that can be called the same as those it comes with, very easy... it's of course your choice where you want to invest your time and it doesn't have to be here if you can find exactly what you need elsewhere by all means, but i just wanted to chime in and maybe provide a little "faith" that this might very well work for you without that much effort if you dig in on the samples provided above and ask questions to folks that are still actively using this lib... i'll help where i can answer a direct question, our needs are pretty simple, e.g. we're not doing multi-language so you'd have to get that kind of help from the other samples posted. i realized a main thing i wanted to point out is that some library communities can be quiet because they're dead and it's best to take that as a sign to move on, but this one is quiet because it has achieved feature maturity (for our needs at least) which is a completely opposite reason to stick around and learn more. |
I've seen the solution you suggested @Beej126. I almost went for it. It seems solid. But we finally decided to drop this feature. We use this library in our backend with NestJS. I was looking for a way to add translation to error messages. But the default messages are good enough (and it's more for a debug purpose since the user will not see it) Indeed, since we also validate the forms before sending them to the backend, we can generate the error messages directly on the frontend client (with the translations). For more complex validations requiring this library, we can at least change the message to return an error code (that we can format on the frontend). Finally, there's a library called NestJs-I18n that has a function for translating error messages. It's not global, but it's already very good. But we've decided not to use it, since we're now handling all this on the frontend. On the other hand, I'm still very disappointed that such a simple feature isn't available... but like @Beej126 said, this library reached maturity. So we shouldn't wait for an update to happen tomorrow. It does an excellent job in all other aspects of data validation. I hope this reflexion will help other people. |
Funciono para mi, en express
Nose porque pero si lo pongo asi: Pero si lo coloco asi, funciona bien
o que seria lo mismo
|
A validation message without the property name will be beneficial. The actual field name may not be the same as the variable name. |
We have developed a wrapper that thinly wraps the class-validator to make the default error messages in a specific language. |
This library just had a new release on jan 12. Any update as regards this issue? |
Will it be ever happening? |
FYI: I did some tweaks with the package which enables you to pass a transformation function at the time of validation to modify the validation error message. you can write your own implementation of the transformation function (which will be called with the name or type of validation, example: isString, isArray and so on) to handle the message as you want, but you can also pass ![]() Also incase you want to provide any specific message for any specific key in your schema then you can pass a transformKey to the ![]() You can play with the live demo here |
My workaround. It seems to be working fine. No need any patch.
|
Enhancing @musaevonline suggestion, allowing for a customizable field name in the response message: Example: @IsString({ context: { fieldName: 'nome do usuário' } }) <= optional custom field name
userName: string; Code: /**
* Arguments being sent to message builders - user can create message either by simply returning a string,
* either by returning a function that accepts MessageArguments and returns a message string built based on these arguments.
*/
export interface ValidationArguments {
/**
* Constraints set by this validation type.
*/
constraints: any[];
/**
* Name of the field that is being validated
*/
fieldName?: string; <= Added this field
/**
* Object that is being validated.
*/
object?: object;
/**
* Name of the object's property being validated.
*/
property: string;
/**
* Name of the target that is being validated.
*/
targetName: string;
/**
* Validating value.
*/
value: any;
} export class ValidationUtils {
static replaceMessageSpecialTokens(
message: ((args: ValidationArguments) => string) | string,
validationArguments: ValidationArguments,
): string {
let messageString: string;
if (message instanceof Function) {
messageString = (message as (args: ValidationArguments) => string)(validationArguments);
} else if (typeof message === 'string') {
messageString = message;
}
if (messageString && Array.isArray(validationArguments.constraints)) {
validationArguments.constraints.forEach((constraint, index) => {
messageString = messageString.replace(
new RegExp(`\\$constraint${index + 1}`, 'g'),
constraintToString(constraint),
);
});
}
if (
messageString &&
validationArguments?.value &&
['boolean', 'number', 'string'].includes(typeof validationArguments.value)
)
messageString = messageString.replace(/\$value/g, validationArguments.value);
if (messageString) {
messageString = messageString.replace(
/\$property/g,
validationArguments.fieldName || validationArguments.property, <= Added this check
);
}
if (messageString) messageString = messageString.replace(/\$target/g, validationArguments.targetName);
return messageString;
}
} // main.ts
app.useGlobalPipes( new ValidationPipe({
exceptionFactory(errors): BadRequestException {
const result: string[] = [];
errors.forEach((error) => {
const validationMetas = getMetadataStorage().getTargetValidationMetadatas(
error.target.constructor,
error.target.constructor.name,
true,
false,
);
const validationMeta = validationMetas.find((meta) => meta.propertyName === error.property);
let errorKey = Object.keys(error.constraints)[0];
const errorMessage = error.constraints[errorKey];
// fix isLength ambiguous message
if (errorKey === 'isLength') {
if (errorMessage.includes('be longer')) {
errorKey = 'minLength';
} else if (errorMessage.includes('be shorter')) {
errorKey = 'maxLength';
} else {
Logger.warn(`Mensagem inesperada para a propriedade 'isLength': ${errorMessage}`);
}
}
const validationArguments: ValidationArguments = {
constraints: validationMeta.constraints,
fieldName: error?.contexts?.[errorKey]?.fieldName,
property: error.property,
targetName: error.target.constructor.name,
value: error.value,
};
const messageTemplate = errorMessages[errorKey];
if (!messageTemplate) {
Logger.warn(`Chave de erro não encontrada na lista de Mensagens de Erro: ${errorKey}`);
}
const message = ValidationUtils.replaceMessageSpecialTokens(messageTemplate, validationArguments);
result.push(message);
});
return new BadRequestException(result);
},
validationError: {
value: true,
},
})
) export const errorMessages = {
arrayContains: 'o campo $property deve conter $constraint1 valores',
arrayMaxSize: 'o campo $property deve conter no máximo $constraint1 elementos',
arrayMinSize: 'o campo $property deve conter pelo menos $constraint1 elementos',
arrayNotContains: 'o campo $property não deve conter $constraint1 valores',
arrayNotEmpty: 'o campo $property não deve estar vazio',
contains: 'o campo $property deve conter a string $constraint1',
equals: 'o campo $property deve ser igual a $constraint1',
isAlpha: 'o campo $property deve conter apenas letras (a-zA-Z)',
isAlphanumeric: 'o campo $property deve conter apenas letras e números',
isArray: 'o campo $property deve ser um array',
isAscii: 'o campo $property deve conter apenas caracteres ASCII',
isBIC: 'o campo $property deve ser um código BIC ou SWIFT',
isBase32: 'o campo $property deve estar codificado no formato base32',
isBase58: 'o campo $property deve estar codificado no formato base58',
isBase64: 'o campo $property deve estar codificado no formato base64',
isBoolean: 'o campo $property deve ser um valor booleano',
isBooleanString: 'o campo $property deve ser uma string que representa um valor booleano',
isBtcAddress: 'o campo $property deve ser um endereço BTC',
isCreditCard: 'o campo $property deve ser um cartão de crédito',
isCurrency: 'o campo $property deve ser uma moeda',
isDataURI: 'o campo $property deve estar no formato data URI',
isDate: 'o campo $property deve ser uma instância de Date',
isDateString: 'o campo $property deve ser uma string de data válida no formato ISO 8601',
isDecimal: 'o campo $property não é um número decimal válido',
isDefined: 'o campo $property não deve ser null ou undefined',
isDivisibleBy: 'o campo $property deve ser divisível por $constraint1',
isEAN: 'o campo $property deve ser um EAN (código de artigo europeu)',
isEmail: 'o campo $property deve ser um endereço de email',
isEmpty: 'o campo $property deve estar vazio',
isEnum: 'o campo $property deve ser um dos seguintes valores: $constraint2',
isEthereumAddress: 'o campo $property deve ser um endereço Ethereum',
isFQDN: 'o campo $property deve ser um nome de domínio válido',
isFirebasePushId: 'o campo $property deve ser um identificador Firebase Push',
isFullWidth: 'o campo $property deve conter caracteres de largura total',
isHSL: 'o campo $property deve ser uma cor no formato HSL',
isHalfWidth: 'o campo $property deve conter caracteres de meia largura',
isHash: 'o campo $property deve ser um hash do tipo $constraint1',
isHexColor: 'o campo $property deve ser uma cor hexadecimal',
isHexadecimal: 'o campo $property deve ser um número hexadecimal',
isIBAN: 'o campo $property deve ser um IBAN',
isIP: 'o campo $property deve ser um endereço IP',
isISBN: 'o campo $property deve ser um ISBN',
isISIN: 'o campo $property deve ser um ISIN (identificador de título/ação)',
isISO4217CurrencyCode: 'o campo $property deve ser um código de moeda ISO4217 válido',
isISO8601: 'o campo $property deve ser uma string de data válida no formato ISO 8601',
isISO31661Alpha2: 'o campo $property deve ser um código ISO31661 Alpha2 válido',
isISO31661Alpha3: 'o campo $property deve ser um código ISO31661 Alpha3 válido',
isISRC: 'o campo $property deve ser um ISRC',
isISSN: 'o campo $property deve ser um ISSN',
isIdentityCard: 'o campo $property deve ser um número de documento de identidade',
isIn: 'o campo $property deve ser um dos seguintes valores: $constraint1',
isInt: 'o campo $property deve ser um número inteiro',
isJSON: 'o campo $property deve ser uma string JSON',
isJWT: 'o campo $property deve ser uma string JWT',
isLatLong: 'o campo $property deve ser uma string de latitude e longitude',
isLatitude: 'o campo $property deve ser uma string ou número de latitude',
isLocale: 'o campo $property deve ser uma localidade',
isLongitude: 'o campo $property deve ser uma string ou número de longitude',
isLowercase: 'o campo $property deve ser uma string em minúsculas',
isMacAddress: 'o campo $property deve ser um endereço MAC',
isMagnetURI: 'o campo $property deve estar no formato magnet URI',
isMilitaryTime: 'o campo $property deve ser uma representação de hora militar válida no formato HH:MM',
isMimeType: 'o campo $property deve ser um tipo MIME válido',
isMobilePhone: 'o campo $property deve ser um número de telefone válido',
isMongoId: 'o campo $property deve ser um identificador MongoDB',
isMultibyte: 'o campo $property deve conter um ou mais caracteres multibyte',
isNegative: 'o campo $property deve ser um número negativo',
isNotEmpty: 'o campo $property não deve estar vazio',
isNotEmptyObject: 'o campo $property deve ser um objeto não vazio',
isNotIn: 'o campo $property não deve ser um dos seguintes valores: $constraint1',
isNumber: 'o campo $property deve ser um número que atenda às restrições especificadas',
isNumberString: 'o campo $property deve ser uma string numérica',
isObject: 'o campo $property deve ser um objeto',
isOctal: 'o campo $property deve ser um número octal válido',
isPassportNumber: 'o campo $property deve ser um número de passaporte válido',
isPhoneNumber: 'o campo $property deve ser um número de telefone válido',
isPort: 'o campo $property deve ser uma porta',
isPositive: 'o campo $property deve ser um número positivo',
isPostalCode: 'o campo $property deve ser um código postal',
isRFC3339: 'o campo $property deve ser uma data no formato RFC 3339',
isRgbColor: 'o campo $property deve ser uma cor RGB',
isSemVer: 'o campo $property deve estar em conformidade com a especificação de versionamento semântico',
isString: 'o campo $property deve ser uma string',
isStrongPassword: 'o campo $property não é seguro o suficiente',
isSurrogatePair: 'o campo $property deve conter qualquer caractere de par substituto',
isTaxId: 'o campo $property deve ser um número de identificação fiscal',
isTimeZone: 'o campo $property deve ser um fuso horário IANA válido',
isUppercase: 'o campo $property deve ser uma string em maiúsculas',
isUrl: 'o campo $property deve ser um URL',
isUuid: 'o campo $property deve ser um UUID',
isVariableWidth: 'o campo $property deve conter caracteres de largura total e meia largura',
matches: 'o campo $property deve corresponder à expressão regular $constraint1',
max: 'o campo $property não deve ser maior que $constraint1',
maxDate: 'a data máxima para o campo $property é $constraint1',
maxLength: 'o campo $property deve ter no máximo $constraint1 caracteres',
min: 'o campo $property não deve ser menor que $constraint1',
minDate: 'a data mínima permitida para o campo $property é $constraint1',
minLength: 'o campo $property deve ter no mínimo $constraint1 caracteres',
notContains: 'o campo $property não deve conter a string $constraint1',
notEquals: 'o campo $property não deve ser igual a $constraint1',
}; |
I was facing the same problem. The package is excellent and easy to use, but the lack of translation support ends up making it difficult to use for those who are developing systems that need to support multiple languages (like in my case). Considering the needs of everyone in this issue, I decided to dedicate some of my time to implement a translation system that supports some languages by default (English, Portuguese (Brazil) and Spanish) and that is extensible, allowing users to customize the feature or integrate with other packages, such as i18n. I've sent a pull request to the developers and am waiting for their approval. |
бомбезно чювак, спасибо за перевод ошибок и реализацию пайпа, только надо заменить
на
иначе он при множественных валидаторах может взять текст и констрейт не того валидатора что в ошибке указан |
Instead of passing custom
message
to each decorator.The text was updated successfully, but these errors were encountered: