Repository: lukeautry/tsoa (ts)

Issue:
ModelValidation does not apply property defaults/coercion
<!--- Provide a general summary of the issue in the Title above -->

## Sorting

* **I'm submitting a ...**
  - [x] bug report
  - [ ] feature request
  - [ ] support request

* I confirm that I
  - [x] used the [search](https://github.com/lukeautry/tsoa/search?type=Issues) to make sure that a similar issue hasn't already been submit

## Expected Behavior
```
interface WithDefaults {
  /**
   * @isInt
   * @default 666
   */
  myNumber?: number;
}
```
sets 666 when no number gets sent in.

## Current Behavior
Generated metadata is correct, however, nothing is set during validation.

This happens because tsoa only sets defaults for required parameters (there is an explicit check).

## Possible Solution
Should set the default if `undefined`

## Breaking change?
potentially

Pull request description:
Model/Array/ObjectLiteral validation: return undefined if errors occured
### All Submissions:

* [x] Have you followed the guidelines in our [Contributing](https://github.com/lukeautry/tsoa/tree/master/docs/CONTRIBUTING.md) document?
* [x] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/lukeautry/tsoa/pulls) for the same update/change?
* [x] Have you written unit tests?
* [x] Have you written unit tests that cover the negative cases (i.e.: if bad data is submitted, does the library respond properly)?
* [x] This PR is associated with an existing issue?

**Closing issues**

Closes #613 

### If this is a new feature submission:

* [x] Has the issue had a maintainer respond to the issue and clarify that the feature is something that aligns with the [goals](https://github.com/lukeautry/tsoa#goal) and [philosophy](https://github.com/lukeautry/tsoa#philosophy) of the project?

**Potential Problems With The Approach**

<!-- if there are areas of the solution that you think might have limitations or potential "gotchas", then discuss them here -->
<!-- if you have many questions, then please consider discussing them in the issue thread (unless you need to share code to illustrate the questions) -->

**Test plan**

<!-- explain why the unit tests that you added are demonstrating that the feature works -->

Patch:
diff --git a/src/routeGeneration/templateHelpers.ts b/src/routeGeneration/templateHelpers.ts
index 2e495578..5ea7d70c 100644
--- a/src/routeGeneration/templateHelpers.ts
+++ b/src/routeGeneration/templateHelpers.ts
@@ -101,6 +101,8 @@ export class ValidationService {
       return;
     }
 
+    const previousErrors = Object.keys(fieldErrors).length;
+
     if (!nestedProperties) {
       throw new Error(
         'internal tsoa error: ' +
@@ -135,7 +137,7 @@ export class ValidationService {
       }
     });
 
-    if (Object.keys(fieldErrors).length > 0) {
+    if (Object.keys(fieldErrors).length > previousErrors) {
       return;
     }
 
@@ -394,7 +396,7 @@ export class ValidationService {
   }
 
   public validateArray(name: string, value: any[], fieldErrors: FieldErrors, swaggerConfig: SwaggerConfigRelatedToRoutes, schema?: TsoaRoute.PropertySchema, validators?: ArrayValidator, parent = '') {
-    if (!schema || value === undefined || value === null) {
+    if (!schema || value === undefined) {
       const message = validators && validators.isArray && validators.isArray.errorMsg ? validators.isArray.errorMsg : `invalid array`;
       fieldErrors[parent + name] = {
         message,
@@ -404,6 +406,7 @@ export class ValidationService {
     }
 
     let arrayValue = [] as any[];
+    const previousErrors = Object.keys(fieldErrors).length;
     if (Array.isArray(value)) {
       arrayValue = value.map((elementValue, index) => {
         return this.ValidateParam(schema, elementValue, `$${index}`, fieldErrors, name + '.', swaggerConfig);
@@ -412,6 +415,10 @@ export class ValidationService {
       arrayValue = [this.ValidateParam(schema, value, '$0', fieldErrors, name + '.', swaggerConfig)];
     }
 
+    if (Object.keys(fieldErrors).length > previousErrors) {
+      return;
+    }
+
     if (!validators) {
       return arrayValue;
     }
@@ -634,6 +641,7 @@ export class ValidationService {
     minimalSwaggerConfig: SwaggerConfigRelatedToRoutes;
   }): any {
     const { name, value, modelDefinition, fieldErrors, parent = '', minimalSwaggerConfig: swaggerConfig } = input;
+    const previousErrors = Object.keys(fieldErrors).length;
 
     if (modelDefinition) {
       if (modelDefinition.dataType === 'refEnum') {
@@ -658,12 +666,11 @@ export class ValidationService {
       const keysOnPropertiesModelDefinition = new Set(Object.keys(properties));
       const allPropertiesOnData = new Set(Object.keys(value));
 
-      keysOnPropertiesModelDefinition.forEach((key: string) => {
-        const property = properties[key];
+      Object.entries(properties).forEach(([key, property]) => {
+        const validatedParam = this.ValidateParam(property, value[key], key, fieldErrors, fieldPath + '.', swaggerConfig);
 
-        // process value only if it exists inside of value or if it is required
-        if (key in value || property.required) {
-          value[key] = this.ValidateParam(property, value[key], key, fieldErrors, fieldPath + '.', swaggerConfig);
+        if (validatedParam !== undefined) {
+          value[key] = validatedParam;
         }
       });
 
@@ -718,6 +725,10 @@ export class ValidationService {
       }
     }
 
+    if (Object.keys(fieldErrors).length > previousErrors) {
+      return;
+    }
+
     return value;
   }
 }


Test patch:
diff --git a/tests/unit/swagger/templateHelpers.spec.ts b/tests/unit/swagger/templateHelpers.spec.ts
index ee12dcc9..bfea8a83 100644
--- a/tests/unit/swagger/templateHelpers.spec.ts
+++ b/tests/unit/swagger/templateHelpers.spec.ts
@@ -844,7 +844,7 @@ describe('ValidationService', () => {
       const value = ['A', 10, true];
       const error = {};
       const result = new ValidationService({}).validateArray(name, value, error, {}, { dataType: 'integer' });
-      expect(result).to.deep.equal([undefined, 10, undefined]);
+      expect(result).to.deep.equal(undefined);
       expect(error[`${name}.$0`].message).to.equal('invalid integer number');
       expect(error[`${name}.$0`].value).to.equal('A');
       expect(error[`${name}.$2`].message).to.equal('invalid integer number');
@@ -863,7 +863,7 @@ describe('ValidationService', () => {
           },
         },
       }).validateArray(name, value, error, {}, { ref: 'ExampleModel' });
-      expect(result).to.deep.equal([{ a: undefined }, { a: 'bcd' }]);
+      expect(result).to.deep.equal(undefined);
       expect(error).to.deep.equal({
         [`${name}.$0.a`]: {
           message: 'invalid string value',