Repository: typestack/class-validator (ts)

Issue:
Provide an option to ValidationError.toString to use constraint messages instead of names only
### Description
The `toString` funtion of the `ValidationError` lists all failed constraints, but uses the constraint name which does not give a hint on why the constraint failed. The constraint message would be much more meaningful and provide more information.

E.g. current default output for `ValidationError.toString`
```
An instance of RootClass has failed the validation:
 - property title has failed the following constraints: minLength, isString
```

What I would like it to be:
```
An instance of RootClass has failed the validation:
 - property title has failed the following constraints: title must be longer than or equal to 15 characters, title must be a string
```

### Proposed solution
Add an optional argument `useConstraintMessages` to the `ValidationError.toString` function that switches from constraint names to constraint messages as output.


Pull request description:
feat: add `showConstraintMessages` option to `ValidationError.toString()`
## Description
Add `useConstraintMessages` option parameter to ValidationError.toString function to use constraint messages instead of constraint names in the string representation for an error.

## Checklist
<!-- Replace  the [ ] with [x] to check the boxes. -->
- [x] the pull request title describes what this PR does (not a vague title like `Update index.md`)
- [x] the pull request targets the *default* branch of the repository (`develop`)
- [x] the code follows the established code style of the repository
  - `npm run prettier:check` passes
  - `npm run lint:check` passes
- [x] tests are added for the changes I made (if any source code was modified)
- [x] documentation added or updated
- [x] I have run the project locally and verified that there are no errors

### Fixes
<!-- If there is no issue being resolved, open one before creating this pull request. -->
<!-- If the PR doesn't fully resolve the issue, replace 'fixes' with 'references'. -->
fixes #1611


Patch:
diff --git a/src/validation/ValidationError.ts b/src/validation/ValidationError.ts
index aa0c501..daa3ff9 100644
--- a/src/validation/ValidationError.ts
+++ b/src/validation/ValidationError.ts
@@ -45,14 +45,20 @@ export class ValidationError {
    * @param shouldDecorate decorate the message with ANSI formatter escape codes for better readability
    * @param hasParent true when the error is a child of an another one
    * @param parentPath path as string to the parent of this property
+   * @param showConstraintMessages show constraint messages instead of constraint names
    */
-  toString(shouldDecorate: boolean = false, hasParent: boolean = false, parentPath: string = ``): string {
+  toString(
+    shouldDecorate: boolean = false,
+    hasParent: boolean = false,
+    parentPath: string = ``,
+    showConstraintMessages: boolean = false
+  ): string {
     const boldStart = shouldDecorate ? `\x1b[1m` : ``;
     const boldEnd = shouldDecorate ? `\x1b[22m` : ``;
+    const constraintsToString = () =>
+      (showConstraintMessages ? Object.values : Object.keys)(this.constraints ?? {}).join(`, `);
     const propConstraintFailed = (propertyName: string): string =>
-      ` - property ${boldStart}${parentPath}${propertyName}${boldEnd} has failed the following constraints: ${boldStart}${Object.keys(
-        this.constraints
-      ).join(`, `)}${boldEnd} \n`;
+      ` - property ${boldStart}${parentPath}${propertyName}${boldEnd} has failed the following constraints: ${boldStart}${constraintsToString()}${boldEnd} \n`;
 
     if (!hasParent) {
       return (
@@ -61,7 +67,9 @@ export class ValidationError {
         }${boldEnd} has failed the validation:\n` +
         (this.constraints ? propConstraintFailed(this.property) : ``) +
         (this.children
-          ? this.children.map(childError => childError.toString(shouldDecorate, true, this.property)).join(``)
+          ? this.children
+              .map(childError => childError.toString(shouldDecorate, true, this.property, showConstraintMessages))
+              .join(``)
           : ``)
       );
     } else {
@@ -75,7 +83,9 @@ export class ValidationError {
       } else {
         return this.children
           ? this.children
-              .map(childError => childError.toString(shouldDecorate, true, `${parentPath}${formattedProperty}`))
+              .map(childError =>
+                childError.toString(shouldDecorate, true, `${parentPath}${formattedProperty}`, showConstraintMessages)
+              )
               .join(``)
           : ``;
       }
diff --git a/src/validation/ValidationExecutor.ts b/src/validation/ValidationExecutor.ts
index 958face..9d3d312 100644
--- a/src/validation/ValidationExecutor.ts
+++ b/src/validation/ValidationExecutor.ts
@@ -45,9 +45,9 @@ export class ValidationExecutor {
      */
     if (!this.metadataStorage.hasValidationMetaData && this.validatorOptions?.enableDebugMessages === true) {
       console.warn(
-        `No validation metadata found. No validation will be  performed. There are multiple possible reasons:\n` + 
-        `  - There may be multiple class-validator versions installed. You will need to flatten your dependencies to fix the issue.\n` + 
-        `  - This validation runs before any file with validation decorator was parsed by NodeJS.`
+        `No validation metadata found. No validation will be  performed. There are multiple possible reasons:\n` +
+          `  - There may be multiple class-validator versions installed. You will need to flatten your dependencies to fix the issue.\n` +
+          `  - This validation runs before any file with validation decorator was parsed by NodeJS.`
       );
     }
 


Test patch:
diff --git a/test/functional/validation-error.spec.ts b/test/functional/validation-error.spec.ts
index b68441a..d2a7f8d 100644
--- a/test/functional/validation-error.spec.ts
+++ b/test/functional/validation-error.spec.ts
@@ -49,6 +49,7 @@ describe('ValidationError', () => {
     }
 
     const validationErrors = await validator.validate(new RootClass());
+    expect(validationErrors).toHaveLength(3);
     expect(validationErrors[0].toString()).toEqual(
       'An instance of RootClass has failed the validation:\n' +
         ' - property title has failed the following constraints: minLength, isString \n'
@@ -67,5 +68,23 @@ describe('ValidationError', () => {
         ' - property nestedArr[1].name has failed the following constraints: isString \n' +
         ' - property nestedArr[1].url has failed the following constraints: isUrl \n'
     );
+    expect(validationErrors[0].toString(undefined, undefined, undefined, true)).toEqual(
+      'An instance of RootClass has failed the validation:\n' +
+        ' - property title has failed the following constraints: title must be longer than or equal to 15 characters, title must be a string \n'
+    );
+    expect(validationErrors[1].toString(undefined, undefined, undefined, true)).toEqual(
+      'An instance of RootClass has failed the validation:\n' +
+        ' - property nestedObj.name has failed the following constraints: name must be a string \n' +
+        ' - property nestedObj.url has failed the following constraints: url must be a URL address \n' +
+        ' - property nestedObj.insideNested.name has failed the following constraints: name must be a string \n' +
+        ' - property nestedObj.insideNested.url has failed the following constraints: url must be a URL address \n'
+    );
+    expect(validationErrors[2].toString(undefined, undefined, undefined, true)).toEqual(
+      'An instance of RootClass has failed the validation:\n' +
+        ' - property nestedArr[0].name has failed the following constraints: name must be a string \n' +
+        ' - property nestedArr[0].url has failed the following constraints: url must be a URL address \n' +
+        ' - property nestedArr[1].name has failed the following constraints: name must be a string \n' +
+        ' - property nestedArr[1].url has failed the following constraints: url must be a URL address \n'
+    );
   });
 });