Repository: hapijs/joi (JavaScript)

Issue:
stripUnknown doesn't work for nested objects
<!--
  ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️
  You must complete this entire issue template to receive support. You MUST NOT remove, change, or replace the template with your own format. A missing or incomplete report will cause your issue to be closed without comment. Please respect the time and experience that went into this template. It is here for a reason. Thank you!
  ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️
-->

#### Support plan

<!--
We are here to help!

Community based support is, by its nature, limited to available community members able to help. Most community support issues are resolved within 2 weeks. Before submitting an issue, please review the various support options available at https://hapi.dev/support/. That page includes useful information about different ways to ask questions.
-->

* *is this issue currently blocking your project?* (yes/no): **yes**
* *is this issue affecting a production system?* (yes/no): **no**

#### Context

* *node version*: **v14.6.0**
* *module version with issue*: 
* *last module version without issue*: 
* *environment* (e.g. node, browser, native): **node**
* *used with* (e.g. hapi application, another framework, standalone, ...): **standalone**
* *any other relevant information*:

#### What are you trying to achieve or the steps to reproduce?

<!--
Describe your issue in detail, including full steps to reproduce the issue, any configuration, schemas, code samples, or inputs needed. Make sure to wrap all code examples in backticks so that they display correctly. Before submitting an issue, make sure to click on the Preview tab above to verify everything is formatted correctly.
-->

It looks like `stripUnknown` doesn't works for nested object when the nested object has invalid fields.

```js
const schema = Joi.object({
    foo: Joi.string(),
    nested: Joi.object({
        bar: Joi.string(),
    })
})

const options = {
    abortEarly: false,
    allowUnknown: true,
    presence: 'required',
    stripUnknown: true,
}

const input = {
    foo: null, // invalid value
    unknown: 'unknown',
    nested: {
        bar: null, // invalid value
        unknown: 'unknown',
    }
}

const { value } = schema.validate(input, options)
// { foo: null, nested: { bar: null, unknown: 'unknown' } }
```

#### What was the result you got?

`nested.unknow` is still present

#### What result did you expect?

It should have been "stripped"

Inconsistency with stripUnknown: true and failing validation rules
<!--
  ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️
  You must complete this entire issue template to receive support. You MUST NOT remove, change, or replace the template with your own format. A missing or incomplete report will cause your issue to be closed without comment. Please respect the time and experience that went into this template. It is here for a reason. Thank you!
  ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️
-->

#### Support plan

<!--
We are here to help!

Community based support is, by its nature, limited to available community members able to help. Most community support issues are resolved within 2 weeks. Before submitting an issue, please review the various support options available at https://hapi.dev/support/. That page includes useful information about different ways to ask questions.
-->

* *is this issue currently blocking your project?* (yes/no): yes
* *is this issue affecting a production system?* (yes/no): yes

#### Context

* *node version*: 12.8.3
* *module version with issue*: 17.1.1
* *last module version without issue*: -
* *environment* (e.g. node, browser, native): node
* *used with* (e.g. hapi application, another framework, standalone, ...): standalone
* *any other relevant information*: -

#### What are you trying to achieve or the steps to reproduce?

<!--
Describe your issue in detail, including full steps to reproduce the issue, any configuration, schemas, code samples, or inputs needed. Make sure to wrap all code examples in backticks so that they display correctly. Before submitting an issue, make sure to click on the Preview tab above to verify everything is formatted correctly.
-->

`stipUnknown` flag works inconsistently when validation error occurs. It only works correctly when validation fails on root level object. however, when it fails on child object - unknown properties are returned in `result.value`. Switching `abortEarly` on/off doesn't make a difference.

In the example below first test succeeds - as expected, and the second one fails.

```js
import { expect } from 'chai';
import { J } from './index';

describe('stripUnknown', () => {

  // - this tests succeeds
  it('should strip nested unknown properties when validation fails at root level', () => {
    const validation = J.object({
      name: J.string().valid('John'), // - failing rule is at root level
      child: J.object({
        anotherChild: J.object({}),
      }),
    });

    const value = {
      name: 'not John',
      child: {
        anotherChild: {
          strip: true,
        },
      },
    };

    const result = validation.validate(value, {
      abortEarly: false,
      stripUnknown: true,
    });

    expect(result.value)
      .to.have.property('child')
      .which.has.property('anotherChild')
      .which.does.not.have.property('strip');
  });

  // - this test fails
  it('should strip nested unknown properties when validation fails in child object', () => {
    const validation = J.object({
      child: J.object({
        name: J.string().valid('John'), // - failing rule is moved to child property
        anotherChild: J.object({}),
      }),
    });

    const value = {
      child: {
        name: 'not John',
        anotherChild: {
          strip: true,
        },
      },
    };

    const result = validation.validate(value, {
      abortEarly: false,
      stripUnknown: true,
    });

    expect(result.value)
      .to.have.property('child')
      .which.has.property('anotherChild')
      .which.does.not.have.property('strip');
  });
});

```

#### What was the result you got?

`result.value` includes unknown properties in some cases

#### What result did you expect?

`result.value` should never include unknown properties if `allowUnknown: false` no matter at what point validation fails.



Discussion:
If validation fails, joi will just stop what it was doing and return an error.
unless the `abortEarly` option has been set to `false`.
In this case everything work as expected, the validation fail but continue and strip unknown fields

```js
const Joi = require('joi');

const schema = Joi.object({
    foo: Joi.string(),
    nested: Joi.object({
        bar: Joi.string(),
    })
})

const options = {
    abortEarly: false,
    allowUnknown: true,
    presence: 'required',
    stripUnknown: true,
}

const input = {
    foo: null, // invalid value
    unknown: 'unknown',
    nested: {
        bar: 'bar', // valid value
        unknown: 'unknown',
    }
}

const { value } = schema.validate(input, options)
// { foo: null, nested: { bar: 'bar' } }
```

There is definitely an inconsistent behavior with nested objects and stripUnknown when abortEarly is false.
Please re-open @hueniverse 





Patch:
diff --git a/lib/types/keys.js b/lib/types/keys.js
index 678d660ab..949f1225c 100755
--- a/lib/types/keys.js
+++ b/lib/types/keys.js
@@ -112,6 +112,10 @@ module.exports = Any.extend({
                         return { value, errors: result.errors };
                     }
 
+                    if (result.value !== undefined) {
+                        value[key] = result.value;
+                    }
+
                     errors.push(...result.errors);
                 }
                 else if (child.schema._flags.result === 'strip' ||


Test patch:
diff --git a/test/types/object.js b/test/types/object.js
index e130e753c..e7d7dfb8f 100755
--- a/test/types/object.js
+++ b/test/types/object.js
@@ -316,6 +316,45 @@ describe('object', () => {
         ]);
     });
 
+    it('should strip unknown values when stripUnknown === true', () => {
+
+        const schema = Joi.object({
+            itemName: Joi.string().required(),
+            nestedItem: Joi.object({
+                nestedItemName: Joi.string().required()
+            }).required()
+        }).required();
+        const prefs = { abortEarly: false, stripUnknown: true };
+
+        // test for valid objects
+        Helper.validate(schema, prefs, [
+            [
+                { itemName: 'John', toStrip: true, nestedItem: { nestedItemName: 'Doe' } },
+                true,
+                { itemName: 'John', nestedItem: { nestedItemName: 'Doe' } }
+            ],
+            [
+                { itemName: 'John', nestedItem: { nestedItemName: 'Doe', toStrip: true } },
+                true,
+                { itemName: 'John', nestedItem: { nestedItemName: 'Doe' } }
+            ]
+        ]);
+
+        // test for invalid object, value to strip in root object
+        let { value } = schema.validate(
+            { itemName: 1, toStrip: true, nestedItem: { nestedItemName: 'Doe' } },
+            prefs
+        );
+        expect(value).to.equal({ itemName: 1, nestedItem: { nestedItemName: 'Doe' } });
+
+        // test for invalid object, value to strip in nested object
+        value = schema.validate(
+            { itemName: 'John', nestedItem: { nestedItemName: 1, toStripNested: true } },
+            prefs
+        ).value;
+        expect(value).to.equal({ itemName: 'John', nestedItem: { nestedItemName: 1 } });
+    });
+
     it('traverses an object and validate all properties in the top level', () => {
 
         const schema = Joi.object({