Repository: Automattic/mongoose (2026-03-23T03:45:51Z)

Issue:
Validation Bypass when Union of Objects is used in Schema
### 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

8.19.3

### Node.js version

24.11.0

### MongoDB server version

"mongodb-memory-server": "^10.3.0"

### Typescript version (if applicable)

_No response_

### Description

If a union of objects is used inside a schema and an arbitrary object is used upon creation, the first Object Schema is used but validation is skipped, at least for the required fields.

Defined field types still validate correctly, and it is not possible to save arbitrary fields.

The same thing happens when you use this with arrays of objects.

### Steps to Reproduce

```tsx
import mongoose from "mongoose";
import { MongoMemoryServer } from "mongodb-memory-server";
import { inspect } from "node:util";

const SubSchema1 = new mongoose.Schema({
  price: { type: Number, required: true },
  title: { type: String },
  isThisSchema1: { type: Boolean },
});

const SubSchema2 = new mongoose.Schema({
  description: { type: String, required: true },
  title: { type: String },
  isThisSchema2: { type: Boolean },
});

const TestSchema = new mongoose.Schema({
  product: {
    type: mongoose.Schema.Types.Union,
    of: [SubSchema1, SubSchema2],
  },
});

const TestModel = mongoose.model("test", TestSchema);

const main = async () => {
  const mongod = await MongoMemoryServer.create();
  const uri = mongod.getUri();
  await mongoose.connect(uri);

  // This should produce an error, since price or description is required.
  await TestModel.create({
    product: {
      title: "string",
      arbitraryNeverSave: true,
      isThisSchema1: true,
      isThisSchema2: true,
    },
  });

  const data = await TestModel.find().lean();

  console.log(inspect(data, { depth: Infinity }));

  process.exit();
};

main();
```


### Expected Behavior

The example should produce an error because the price or description should be defined.


Discussion:
Hey @vkarpov15 , I would like to work on this issue. Please assign it to me. 
Hey @vkarpov15 , I have fixed this issue and raised the PR #15734 . Please review it and merge it if implemented correctly. 



Patch:
diff --git a/lib/document.js b/lib/document.js
index eda744170c..7b199e0128 100644
--- a/lib/document.js
+++ b/lib/document.js
@@ -3070,7 +3070,9 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip, isNestedValidate
       !_pathType.$parentSchemaDocArray &&
       // gh-15957: skip this optimization for SchemaMap as maps can contain subdocuments
       // that need validation even if the map itself has no validators
-      !_pathType.$isSchemaMap) {
+      !_pathType.$isSchemaMap &&
+      // gh-15732: skip this optimization for Union as it can contain subdocuments
+      !_pathType.$isSchemaUnion) {
       paths.delete(path);
     } else if (_pathType.$isMongooseArray &&
       !_pathType.$isMongooseDocumentArray && // Skip document arrays...
diff --git a/lib/schema/union.js b/lib/schema/union.js
index 7615afb5af..0dc53e6fed 100644
--- a/lib/schema/union.js
+++ b/lib/schema/union.js
@@ -28,6 +28,7 @@ class Union extends SchemaType {
       throw new Error('Union schema type requires an array of types');
     }
     this.schemaTypes = options.of.map(obj => parentSchema.interpretAsType(key, obj, schemaOptions));
+    this.$isSchemaUnion = true;
   }
 
   cast(val, doc, init, prev, options) {
@@ -90,6 +91,32 @@ class Union extends SchemaType {
     throw lastError;
   }
 
+  async doValidate(value, scope, options) {
+    if (options && options.skipSchemaValidators) {
+      if (value != null && typeof value.validate === 'function') {
+        return value.validate();
+      }
+      return;
+    }
+
+    await super.doValidate(value, scope, options);
+    if (value != null && typeof value.validate === 'function') {
+      await value.validate();
+    }
+  }
+
+  doValidateSync(value, scope, options) {
+    if (!options || !options.skipSchemaValidators) {
+      const schemaTypeError = super.doValidateSync(value, scope, options);
+      if (schemaTypeError) {
+        return schemaTypeError;
+      }
+    }
+    if (value != null && typeof value.validateSync === 'function') {
+      return value.validateSync();
+    }
+  }
+
   clone() {
     const schematype = super.clone();
 
diff --git a/types/utility.d.ts b/types/utility.d.ts
index e93f1774db..19d7f8e231 100644
--- a/types/utility.d.ts
+++ b/types/utility.d.ts
@@ -98,10 +98,10 @@ declare module 'mongoose' {
   type UnpackedIntersection<T, U> = T extends null
     ? null
     : T extends (infer A)[]
-    ? (Omit<A, keyof U> & U)[]
+    ? (A extends any ? (Omit<A, keyof U> & U) : never)[]
     : keyof U extends never
     ? T
-    : Omit<T, keyof U> & U;
+    : T extends any ? (Omit<T, keyof U> & U) : never;
 
   type MergeType<A, B> = A extends unknown ? Omit<A, keyof B> & B : never;
 


Test patch:
diff --git a/test/model.test.js b/test/model.test.js
index 0a87970127..9e33a42e8c 100644
--- a/test/model.test.js
+++ b/test/model.test.js
@@ -4341,7 +4341,7 @@ describe('Model', function() {
 
           let lastUse = session.serverSession.lastUse;
 
-          await delay(1);
+          await delay(10);
 
           const docs = await MyModel.find({ _id: doc._id }, null,
             { session: session });
@@ -4352,7 +4352,7 @@ describe('Model', function() {
           assert.ok(session.serverSession.lastUse > lastUse);
           lastUse = session.serverSession.lastUse;
 
-          await delay(1);
+          await delay(10);
 
           docs[0].name = 'test3';
 
diff --git a/test/schema.union.test.js b/test/schema.union.test.js
index 0b231aae38..8ae4077d2e 100644
--- a/test/schema.union.test.js
+++ b/test/schema.union.test.js
@@ -158,4 +158,54 @@ describe('Union', function() {
     assert.strictEqual(found.arr[0], numValue);
     assert.strictEqual(new Date(found.arr[1]).valueOf(), dateValue.valueOf());
   });
+  it('does not bypass validation when a Union of Objects is used (gh-15732)', async function() {
+    const SubSchema1 = new Schema({
+      price: { type: Number, required: true },
+      title: { type: String },
+      isThisSchema1: { type: Boolean }
+    }, { _id: false });
+
+    const TestSchema = new Schema({
+      product: {
+        type: 'Union',
+        of: [SubSchema1, Number]
+      }
+    });
+
+    const TestModel = db.model('Test', TestSchema);
+
+    // This should fail validation because neither required 'price' nor 'description' are provided
+    const doc = new TestModel({
+      product: {
+        title: 'string',
+        arbitraryNeverSave: true,
+        isThisSchema1: true,
+        isThisSchema2: true
+      }
+    });
+
+    let err;
+    try {
+      await doc.validate();
+    } catch (e) {
+      err = e;
+    }
+
+    assert.ok(err, 'Expected validation error');
+    assert.ok(
+      err.errors['product.price'] || err.errors['product.description'] || err.errors['product'],
+      'Expected missing required property error'
+    );
+
+    const doc2 = new TestModel({
+      product: {
+        price: 42,
+        title: 'string',
+        arbitraryNeverSave: true,
+        isThisSchema1: true,
+        isThisSchema2: true
+      }
+    });
+    await doc2.validate();
+  });
 });