Repository: lukeautry/tsoa (ts)

Issue:
about undefined properties in tsoa request body
## Sorting

* **I'm submitting a ...**
  - [X] bug report
  - [ ] feature request
  - [X] 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

## Issue / Question

I have a simple input post body e.g.: 
```ts
interface InputDTO {
	firstName?: string;
	lastName?: string;
	email?: string;
	phone?: string;
	birthday?: Date;
	gender?: Gender;
	nationality?: string;
}
 enum Gender {
	Male = 'male',
	Female = 'female'
}

     @Post('/test')
	async test(
		@Request() req,
		@Body() body: InputDTO
	): Promise<{
		userUpdate: { [key: string]: any };
	}> {
		console.log('body', body);
       }
```

and if I call it with "firstName" only, all other properties are stil there, but undefined.
```js
{
  firstName: 'safd',
  lastName: undefined,
  email: undefined,
  phone: undefined,
  birthday: undefined,
  gender: undefined,
  nationality: undefined
}
```

the question is, is this expected? Therefore I cannot distinguish between calling "firstName: undefined" and firstName not provided at all.
The check 
```js 
if ('firstName' in body) {
    console.log('firstName provided');
}
``` 
is always true.

## Possible Solution
Only set values if they are provided and set undefined as fallback if the property exists in the request but has no value.

## Context (Environment)
Version of the library: tsoa@2.5.8
Version of NodeJS: v12.13.0


Pull request:
fix(model-validation): only set properties in tsoa request body if provided or required
### 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 #525 

**Potential Problems With The Approach**

it changes non defined properties in body (in models) so that they do not exist in there anymore, before that they were undefined even if they were not provided. So far as I understand this is a more correct approach and shouldn't break anything.

**Test plan**

I instanciate the validator and check if non provided values are still show up, besides that I check if require fields are still valiated even if they are not provided.

Patch:
diff --git a/src/routeGeneration/templateHelpers.ts b/src/routeGeneration/templateHelpers.ts
index 8712c778..b5fbd1ec 100644
--- a/src/routeGeneration/templateHelpers.ts
+++ b/src/routeGeneration/templateHelpers.ts
@@ -622,7 +622,11 @@ export class ValidationService {
 
       keysOnPropertiesModelDefinition.forEach((key: string) => {
         const property = properties[key];
-        value[key] = this.ValidateParam(property, value[key], key, fieldErrors, parent, 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, parent, swaggerConfig);
+        }
       });
 
       const isAnExcessProperty = (objectKeyThatMightBeExcess: string) => {


Test patch:
diff --git a/tests/unit/swagger/templateHelpers.spec.ts b/tests/unit/swagger/templateHelpers.spec.ts
index 91fa4c79..900aad8c 100644
--- a/tests/unit/swagger/templateHelpers.spec.ts
+++ b/tests/unit/swagger/templateHelpers.spec.ts
@@ -207,7 +207,7 @@ describe('ValidationService', () => {
       };
       const result = v.validateModel({ name: '', value: {}, refName: 'ExampleModel', fieldErrors: error, minimalSwaggerConfig });
       expect(Object.keys(error)).to.be.empty;
-      expect(result).to.eql({ a: undefined });
+      expect(result).to.eql({});
     });
 
     it('should validate a model with additional properties', () => {
@@ -246,7 +246,7 @@ describe('ValidationService', () => {
       };
       const result = v.validateModel({ name: '', value: {}, refName: 'ExampleModel', fieldErrors: error, minimalSwaggerConfig });
       expect(Object.keys(error)).to.be.empty;
-      expect(result).to.eql({ a: undefined });
+      expect(result).to.eql({});
     });
 
     it('should validate additional properties only against non-explicitly stated properties', () => {
@@ -272,6 +272,71 @@ describe('ValidationService', () => {
       expect(Object.keys(error)).to.be.empty;
       expect(result).to.eql({ a: 9 });
     });
+
+    it('non provided parameters should not result in undefined', () => {
+      const v = new ValidationService({
+        BEnum: {
+          dataType: 'refEnum',
+          enums: ['X', 'Y'],
+        },
+        General: {
+          dataType: 'refObject',
+          properties: {
+            a: { dataType: 'string' },
+            b: { ref: 'BEnum' },
+            c: { dataType: 'string' },
+          },
+          additionalProperties: false,
+        },
+      });
+
+      const error = {};
+
+      const result = v.ValidateParam(
+        { required: true, ref: 'General' },
+        {
+          a: 'value',
+          b: undefined,
+        },
+        'body',
+        error,
+        undefined,
+        {},
+      );
+
+      expect(result).to.deep.equal({ a: 'value', b: undefined });
+      expect(Object.keys(error)).to.be.empty;
+      expect('a' in result).to.be.true; // provided
+      expect('b' in result).to.be.true; // provided, but empty
+      expect('c' in result).to.be.false; // not provided
+    });
+
+    it('required provided parameters should result in required errors', () => {
+      const v = new ValidationService({
+        General: {
+          dataType: 'refObject',
+          properties: {
+            a: { dataType: 'string', required: true },
+          },
+          additionalProperties: false,
+        },
+      });
+
+      const error: any = {};
+
+      v.ValidateParam(
+        { required: true, ref: 'General' },
+        {
+          c: 'value',
+        },
+        'body',
+        error,
+        undefined,
+        {},
+      );
+
+      expect(error.a.message).to.equal(`'a' is required`);
+    });
   });
 
   describe('Integer validate', () => {