Repository: Automattic/mongoose (JavaScript)

Issue:
Schema level discriminator does not allow number values
### 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.4.5

### Node.js version

18.16

### MongoDB server version

6.0.5

### Typescript version (if applicable)

_No response_

### Description

Using a schema level discriminator `schema.discriminator` only accepts string discriminator values. If a number is passed, it will cast it to a string - If the discriminator field is defined on the schema as a Number, it will fail validation when creating/fetching the document.

This is in contrast for when you use schema.path("field").discriminator that works with either a number or a string.

### Steps to Reproduce

```'use strict';

const mongoose = require('mongoose');
const { Schema } = mongoose;

const baseClassSchema = new Schema({
    type: { type: Number, required: true },
}, { discriminatorKey: "type" });

class BaseClass {
    type = 1;

    whoAmI() {
        return "I am base";
    }
}

baseClassSchema.loadClass(BaseClass);
const baseClassModel = mongoose.model('base_class', baseClassSchema);

class NumberTyped extends BaseClass {
    type = 2;

    whoAmI() {
        return "I am NumberTyped";
    }
}

class StringTyped extends BaseClass {
    type = "3";

    whoAmI() {
        return "I am StringTyped";
    }
}

baseClassSchema.discriminator(2, new Schema({}).loadClass(NumberTyped));
baseClassSchema.discriminator("3", new Schema({}).loadClass(StringTyped));

const containerSchema = new Schema({item: baseClassSchema});
containerSchema.path("item").discriminator(4, new Schema({}).loadClass(NumberTyped));
const containerModel = mongoose.model('container', containerSchema);


void async function main() 
{
    await mongoose.connect("mongodb://127.0.0.1:27017/example");
    mongoose.connection.db.dropDatabase();

    let instance = await containerModel.create({item: {type: 4}}); // "I am NumberTyped" - Works
    console.log(`${instance.item.whoAmI()}`);

    instance = await containerModel.create({item: {type: "3"}}); // "I am StringTyped" Works
    console.log(`${instance.item.whoAmI()}`);

    instance = await containerModel.create({item: {type: 2}}); // Uncaught ValidationError ValidationError: container validation failed: item.type: Cast to Number failed for value "2" (type number) at path "type"
    console.log(`${instance.item.whoAmI()}`);
}();
```

### Expected Behavior

The two methods of defining a discriminator should either both accept only strings or both accept other types. With strong preference for the second option, of course!


Discussion:

Patch:
diff --git a/lib/index.js b/lib/index.js
index 6c551ae5dfa..8affae77804 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -624,8 +624,8 @@ Mongoose.prototype._model = function(name, schema, collection, options) {
   connection.emit('model', model);
 
   if (schema._applyDiscriminators != null) {
-    for (const disc of Object.keys(schema._applyDiscriminators)) {
-      model.discriminator(disc, schema._applyDiscriminators[disc]);
+    for (const disc of schema._applyDiscriminators.keys()) {
+      model.discriminator(disc, schema._applyDiscriminators.get(disc));
     }
   }
 
diff --git a/lib/schema.js b/lib/schema.js
index 261211c1fb6..b608961b753 100644
--- a/lib/schema.js
+++ b/lib/schema.js
@@ -449,7 +449,7 @@ Schema.prototype._clone = function _clone(Constructor) {
     s.discriminators = Object.assign({}, this.discriminators);
   }
   if (this._applyDiscriminators != null) {
-    s._applyDiscriminators = Object.assign({}, this._applyDiscriminators);
+    s._applyDiscriminators = new Map(this._applyDiscriminators);
   }
 
   s.aliases = Object.assign({}, this.aliases);
@@ -621,7 +621,8 @@ Schema.prototype.defaultOptions = function(options) {
  * @api public
  */
 Schema.prototype.discriminator = function(name, schema) {
-  this._applyDiscriminators = Object.assign(this._applyDiscriminators || {}, { [name]: schema });
+  this._applyDiscriminators = this._applyDiscriminators || new Map();
+  this._applyDiscriminators.set(name, schema);
 
   return this;
 };
@@ -722,18 +723,18 @@ 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 && utils.isPOJO(val[0]._applyDiscriminators)) {
-        const applyDiscriminators = val[0]._applyDiscriminators || [];
+      } 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 in applyDiscriminators) {
-          schemaType.discriminator(disc, applyDiscriminators[disc]);
+        for (const disc of applyDiscriminators.keys()) {
+          schemaType.discriminator(disc, applyDiscriminators.get(disc));
         }
       }
-      else if (val != null && val.instanceOfSchema && utils.isPOJO(val._applyDiscriminators)) {
-        const applyDiscriminators = val._applyDiscriminators || [];
+      else if (val != null && val.instanceOfSchema && val._applyDiscriminators instanceof Map) {
+        const applyDiscriminators = val._applyDiscriminators;
         const schemaType = this.path(prefix + key);
-        for (const disc in applyDiscriminators) {
-          schemaType.discriminator(disc, applyDiscriminators[disc]);
+        for (const disc of applyDiscriminators.keys()) {
+          schemaType.discriminator(disc, applyDiscriminators.get(disc));
         }
       }
     } else if (Object.keys(val).length < 1) {


Test patch:
diff --git a/test/schema.test.js b/test/schema.test.js
index 1f7a028f1b8..f41620101dc 100644
--- a/test/schema.test.js
+++ b/test/schema.test.js
@@ -2873,6 +2873,43 @@ describe('schema', function() {
     assert(batch.message);
   });
 
+  it('supports numbers with Schema.discriminator() (gh-13788)', async() => {
+    const baseClassSchema = new Schema({
+      type: { type: Number, required: true }
+    }, { discriminatorKey: 'type' });
+
+    class BaseClass {
+      whoAmI() {
+        return 'I am base';
+      }
+    }
+    BaseClass.type = 1;
+
+    baseClassSchema.loadClass(BaseClass);
+
+    class NumberTyped extends BaseClass {
+      whoAmI() {
+        return 'I am NumberTyped';
+      }
+    }
+    NumberTyped.type = 2;
+
+    class StringTyped extends BaseClass {
+      whoAmI() {
+        return 'I am StringTyped';
+      }
+    }
+    StringTyped.type = '3';
+
+    baseClassSchema.discriminator(2, new Schema({}).loadClass(NumberTyped));
+    baseClassSchema.discriminator('3', new Schema({}).loadClass(StringTyped));
+    const Test = db.model('Test', { item: baseClassSchema });
+    let doc = await Test.create({ item: { type: 2 } });
+    assert.equal(doc.item.whoAmI(), 'I am NumberTyped');
+    doc = await Test.create({ item: { type: '3' } });
+    assert.equal(doc.item.whoAmI(), 'I am StringTyped');
+  });
+
   it('can use on as a schema property (gh-11580)', async() => {
     const testSchema = new mongoose.Schema({
       on: String