Repository: hapijs/joi (JavaScript)

Issue:
defaults in alternatives should work recursively
#### Support plan

* *is this issue currently blocking your project?* (yes/no): yes
* *is this issue affecting a production system?* (yes/no): yes

#### Context

* *node version*: 14
* *module version with issue*: 17.4.x
* *last module version without issue*:  n/a
* *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?

Carrying on from the work in #2535, it's reasonable that the defaults in following case should work: 

```js
const schema = Joi.alternatives([
    Joi.alternatives(
        Joi.object({ foo: Joi.string().default('bar') }).unknown(),
        Joi.object({ baz: Joi.boolean().default(false) }).unknown()
    ).match('all'),
    Joi.object({ lol: Joi.boolean().default(true) }).unknown()
]).match('all');
```

However my solution for #2535 didn't handle the case of recursive sets of object `alternatives` being present in the `matches` set.

#### What was the result you got?
```js
schema.validate({})
// {}
```

#### What result did you expect?
```js
schema.validate({})
// { foo: 'bar', baz: false, lol: true }
```


Discussion:

Patch:
diff --git a/lib/types/alternatives.js b/lib/types/alternatives.js
index 875f2b090..5012915b9 100755
--- a/lib/types/alternatives.js
+++ b/lib/types/alternatives.js
@@ -73,8 +73,15 @@ module.exports = Any.extend({
                 return { errors: error('alternatives.all') };
             }
 
-            const allobj = schema.$_terms.matches.reduce((acc, v) => acc && v.schema.type === 'object', true);
-            return allobj ? { value: matched.reduce((acc, v) => Merge(acc, v, { mergeArrays: false })) } : { value: matched[matched.length - 1] };
+            const isAllObj = (alternative) => {
+
+                return alternative.$_terms.matches.reduce(
+                    (acc, v) => acc && (v.schema.type === 'object' || (v.schema.type === 'alternatives' && isAllObj(v.schema))),
+                    true
+                );
+            };
+
+            return isAllObj(schema) ? { value: matched.reduce((acc, v) => Merge(acc, v, { mergeArrays: false })) } : { value: matched[matched.length - 1] };
         }
 
         // Match any


Test patch:
diff --git a/test/types/alternatives.js b/test/types/alternatives.js
index 25fcf95cf..a2a5bfe60 100755
--- a/test/types/alternatives.js
+++ b/test/types/alternatives.js
@@ -1473,6 +1473,22 @@ describe('alternatives', () => {
             ]);
         });
 
+        it('merges defaults when nesting alternatives', () => {
+
+            const schema = Joi.alternatives([
+                Joi.alternatives(
+                    Joi.object({ foo: Joi.string().default('bar') }).unknown(),
+                    Joi.object({ baz: Joi.boolean().default(false) }).unknown()
+                ).match('all'),
+                Joi.object({ lol: Joi.boolean().default(true) }).unknown()
+            ]).match('all');
+
+            Helper.validate(schema, [
+                [{}, true, { foo: 'bar', baz: false, lol: true }],
+                [{ foo: 'rofl' }, true, { foo: 'rofl', baz: false, lol: true }]
+            ]);
+        });
+
         it('errors on mix with conditional', () => {
 
             expect(() => Joi.alternatives().match('all').conditional('$a', { is: true, then: false })).to.throw('Cannot combine match mode all with conditional rule');