Repository: Automattic/mongoose (JavaScript)

Issue:
Discriminated schemas nested with discriminated schemas don't instantiate the correct types
### Prerequisites

- [X] I have written a descriptive issue title
- [X] I have searched existing issues to ensure the bug has not already been reported


### Mongoose version

7.5.3

### Node.js version

18.16

### MongoDB server version

6.0.5

### Typescript version (if applicable)

_No response_

### Description

When defining a discriminated schema A that has a field which is discriminated schema B, after applying the discriminators at the schema level and instantiating an object of schema A, the types of B within it will always be of the default base type.

If you apply the discriminators for schema B directly on the path, it works as expected. This issue also happens if schema A has a subtype that self-references A. I am not sure how easy it is to convery through text, but I hope the code example makes it clear!

### Steps to Reproduce

```javascript
'use strict';

const mongoose = require('mongoose');
const { Schema } = mongoose;

const baseNestedDiscriminated = new Schema({
    type: { type: Number, required: true },
}, { discriminatorKey: "type" });

class BaseClass {
    type = 1;

    whoAmI() {
        return "I am baseNestedDiscriminated";
    }
}

baseNestedDiscriminated.loadClass(BaseClass);

class NumberTyped extends BaseClass {
    type = 3;

    whoAmI() {
        return "I am NumberTyped";
    }
}

class StringTyped extends BaseClass {
    type = 4;

    whoAmI() {
        return "I am StringTyped";
    }
}

baseNestedDiscriminated.discriminator(1, new Schema({}).loadClass(NumberTyped));
baseNestedDiscriminated.discriminator("3", new Schema({}).loadClass(StringTyped));

const outerDiscriminatedSchema = new Schema({
    type: { type: Number, required: true },
}, { discriminatorKey: "type" });

const containsNestedSchema = new Schema({
    nestedDiscriminatedTypes: { type: [baseNestedDiscriminated], required: true },
})

// If you uncomment these, the example works as expected
//containsNestedSchema.path("nestedDiscriminatedTypes").discriminator(1, new Schema({}).loadClass(NumberTyped));
//containsNestedSchema.path("nestedDiscriminatedTypes").discriminator("3", new Schema({}).loadClass(StringTyped));

class ContainsNested {
    nestedDiscriminatedTypes = [];
    type = 1;

    whoAmI() {
        return "I am ContainsNested";
    }
}
containsNestedSchema.loadClass(ContainsNested);

outerDiscriminatedSchema.discriminator(1, containsNestedSchema);
const secondModel = mongoose.model("containsNestedSchema", containsNestedSchema);
const containerModel = mongoose.model("container", new Schema({items: [baseNestedDiscriminated]}));

void async function main() 
{
    await mongoose.connect("mongodb://127.0.0.1:27017/example");
    mongoose.connection.db.dropDatabase();

    let instance = await containerModel.create({ items: [{type: 1}] }); // "I am NumberTyped" - Works
    console.log(instance.items[0].whoAmI());

    instance = await containerModel.create({ items: [{type: "3"}] }); // "I am StringTyped" Works
    console.log(instance.items[0].whoAmI());

    instance = await secondModel.create({ type: 1, nestedDiscriminatedTypes: [{type: 1}, {type: "3"}] });
    
    console.log(instance.whoAmI()); // "I am ContainsNested" - Works
    instance.nestedDiscriminatedTypes.forEach(item => console.log(item.whoAmI()));
    // Expected:
    // I am NumberTyped
    // I am StringTyped
    // Actual:
    // I am baseNestedDiscriminated
    // I am baseNestedDiscriminated

    mongoose.disconnect();
}();
```
### Expected Behavior

Shown in the code example, once discriminators are applied to a schema, the discriminators should always run wherever the schema is.


Discussion:

Patch:
diff --git a/lib/schema.js b/lib/schema.js
index b608961b753..e8fc8f6828f 100644
--- a/lib/schema.js
+++ b/lib/schema.js
@@ -723,19 +723,6 @@ Schema.prototype.add = function add(obj, prefix) {
         for (const key in val[0].discriminators) {
           schemaType.discriminator(key, val[0].discriminators[key]);
         }
-      } else if (val[0] != null && val[0].instanceOfSchema && val[0]._applyDiscriminators instanceof Map) {
-        const applyDiscriminators = val[0]._applyDiscriminators;
-        const schemaType = this.path(prefix + key);
-        for (const disc of applyDiscriminators.keys()) {
-          schemaType.discriminator(disc, applyDiscriminators.get(disc));
-        }
-      }
-      else if (val != null && val.instanceOfSchema && val._applyDiscriminators instanceof Map) {
-        const applyDiscriminators = val._applyDiscriminators;
-        const schemaType = this.path(prefix + key);
-        for (const disc of applyDiscriminators.keys()) {
-          schemaType.discriminator(disc, applyDiscriminators.get(disc));
-        }
       }
     } else if (Object.keys(val).length < 1) {
       // Special-case: {} always interpreted as Mixed path so leaf at this node
diff --git a/lib/schema/SubdocumentPath.js b/lib/schema/SubdocumentPath.js
index 2f8e32d5ace..17f8a5aa79b 100644
--- a/lib/schema/SubdocumentPath.js
+++ b/lib/schema/SubdocumentPath.js
@@ -55,6 +55,12 @@ function SubdocumentPath(schema, path, options) {
   this.$isSingleNested = true;
   this.base = schema.base;
   SchemaType.call(this, path, options, 'Embedded');
+
+  if (schema._applyDiscriminators != null) {
+    for (const disc of schema._applyDiscriminators.keys()) {
+      this.discriminator(disc, schema._applyDiscriminators.get(disc));
+    }
+  }
 }
 
 /*!
diff --git a/lib/schema/documentarray.js b/lib/schema/documentarray.js
index 7f8a39a04d6..a15a1ca2e79 100644
--- a/lib/schema/documentarray.js
+++ b/lib/schema/documentarray.js
@@ -88,6 +88,12 @@ function DocumentArrayPath(key, schema, options, schemaOptions) {
 
   this.$embeddedSchemaType.caster = this.Constructor;
   this.$embeddedSchemaType.schema = this.schema;
+
+  if (schema._applyDiscriminators != null) {
+    for (const disc of schema._applyDiscriminators.keys()) {
+      this.discriminator(disc, schema._applyDiscriminators.get(disc));
+    }
+  }
 }
 
 /**


Test patch:
diff --git a/test/document.test.js b/test/document.test.js
index f3f557350dd..a074a9d9ee2 100644
--- a/test/document.test.js
+++ b/test/document.test.js
@@ -12520,6 +12520,56 @@ describe('document', function() {
     await doc.save();
     assert.strictEqual(attachmentSchemaPreValidateCalls, 1);
   });
+
+  it('handles embedded discriminators defined using Schema.prototype.discriminator (gh-13898)', async function() {
+    const baseNestedDiscriminated = new Schema({
+      type: { type: Number, required: true }
+    }, { discriminatorKey: 'type' });
+
+    class BaseClass {
+      whoAmI() {
+        return 'I am baseNestedDiscriminated';
+      }
+    }
+    BaseClass.type = 1;
+
+    baseNestedDiscriminated.loadClass(BaseClass);
+
+    class NumberTyped extends BaseClass {
+      whoAmI() {
+        return 'I am NumberTyped';
+      }
+    }
+    NumberTyped.type = 3;
+
+    class StringTyped extends BaseClass {
+      whoAmI() {
+        return 'I am StringTyped';
+      }
+    }
+    StringTyped.type = 4;
+
+    baseNestedDiscriminated.discriminator(1, new Schema({}).loadClass(NumberTyped));
+    baseNestedDiscriminated.discriminator('3', new Schema({}).loadClass(StringTyped));
+
+    const containsNestedSchema = new Schema({
+      nestedDiscriminatedTypes: { type: [baseNestedDiscriminated], required: true }
+    });
+
+    class ContainsNested {
+      whoAmI() {
+        return 'I am ContainsNested';
+      }
+    }
+    containsNestedSchema.loadClass(ContainsNested);
+
+    const Test = db.model('Test', containsNestedSchema);
+    const instance = await Test.create({ type: 1, nestedDiscriminatedTypes: [{ type: 1 }, { type: '3' }] });
+    assert.deepStrictEqual(
+      instance.nestedDiscriminatedTypes.map(i => i.whoAmI()),
+      ['I am NumberTyped', 'I am StringTyped']
+    );
+  });
 });
 
 describe('Check if instance function that is supplied in schema option is availabe', function() {