Migrating from 6.x to 7.x

There are several backwards-breaking changes you should be aware of when migrating from Mongoose 6.x to Mongoose 7.x.

If you're still on Mongoose 5.x, please read the Mongoose 5.x to 6.x migration guide and upgrade to Mongoose 6.x first.

strictQuery

strictQuery is now false by default.

const mySchema = new Schema({ field: Number });
const MyModel = mongoose.model('Test', mySchema);

// Mongoose will not strip out `notInSchema: 1` because `strictQuery` is false by default
const docs = await MyModel.find({ notInSchema: 1 });
// Empty array in Mongoose 7. In Mongoose 6, this would contain all documents in MyModel
docs;

Removed remove()

The remove() method on documents and models has been removed. Use deleteOne() or deleteMany() instead.

const mySchema = new Schema({ field: Number });
const MyModel = mongoose.model('Test', mySchema);

// Change this:
await MyModel.remove(filter);

// To this:
await MyModel.deleteOne(filter);
// Or this, if you want to delete multiple:
await MyModel.deleteMany(filter);

// For documents, change this:
await doc.remove();

// To this:
await doc.deleteOne();

Keep in mind that deleteOne() hooks are treated as query middleware by default. So for middleware, please do the following:

// Replace this:
schema.pre('remove', function() {
  /* ... */
});

// With this:
schema.pre('deleteOne', { document: true, query: false }, function() {
  /* ... */
});

Dropped callback support

The following functions no longer accept callbacks. They always return promises.

  • Aggregate.prototype.exec
  • Aggregate.prototype.explain
  • AggregationCursor.prototype.close
  • AggregationCursor.prototype.next
  • AggregationCursor.prototype.eachAsync
  • Connection.prototype.startSession
  • Connection.prototype.dropCollection
  • Connection.prototype.createCollection
  • Connection.prototype.dropDatabase
  • Connection.prototype.openUri
  • Connection.prototype.close
  • Connection.prototype.destroy
  • Document.prototype.populate
  • Document.prototype.validate
  • Mongoose.prototype.connect
  • Mongoose.prototype.createConnection
  • Model.prototype.save
  • Model.aggregate
  • Model.bulkWrite
  • Model.cleanIndexes
  • Model.countDocuments
  • Model.create
  • Model.createCollection
  • Model.createIndexes
  • Model.deleteOne
  • Model.deleteMany
  • Model.distinct
  • Model.ensureIndexes
  • Model.estimatedDocumentCount
  • Model.exists
  • Model.find
  • Model.findById
  • Model.findByIdAndUpdate
  • Model.findByIdAndReplace
  • Model.findOne
  • Model.findOneAndDelete
  • Model.findOneAndUpdate
  • Model.findOneAndRemove
  • Model.insertMany
  • Model.listIndexes
  • Model.replaceOne
  • Model.syncIndexes
  • Model.updateMany
  • Model.updateOne
  • Query.prototype.find
  • Query.prototype.findOne
  • Query.prototype.findOneAndDelete
  • Query.prototype.findOneAndUpdate
  • Query.prototype.findOneAndRemove
  • Query.prototype.findOneAndReplace
  • Query.prototype.validate
  • Query.prototype.deleteOne
  • Query.prototype.deleteMany
  • Query.prototype.exec
  • QueryCursor.prototype.close
  • QueryCursor.prototype.next
  • QueryCursor.prototype.eachAsync

If you are using the above functions with callbacks, we recommend switching to async/await, or promises if async functions don't work for you. If you need help refactoring a legacy codebase, this tool from Mastering JS callbacks to async await using ChatGPT.

// Before
conn.startSession(function(err, session) {
  // ...
});

// After
const session = await conn.startSession();
// Or:
conn.startSession().then(sesson => { /* ... */ });

// With error handling
try {
  await conn.startSession();
} catch (err) { /* ... */ }
// Or:
const [err, session] = await conn.startSession().then(
  session => ([null, session]),
  err => ([err, null])
);

Removed update()

Model.update(), Query.prototype.update(), and Document.prototype.update() have been removed. Use updateOne() instead.

// Before
await Model.update(filter, update);
await doc.update(update);

// After
await Model.updateOne(filter, update);
await doc.updateOne(update);

ObjectId requires new

In Mongoose 6 and older, you could define a new ObjectId without using the new keyword:

// Works in Mongoose 6
// Throws "Class constructor ObjectId cannot be invoked without 'new'" in Mongoose 7
const oid = mongoose.Types.ObjectId('0'.repeat(24));

In Mongoose 7, ObjectId is now a JavaScript class, so you need to use the new keyword.

// Works in Mongoose 6 and Mongoose 7
const oid = new mongoose.Types.ObjectId('0'.repeat(24));

Discriminator schemas use base schema options by default

When you use Model.discriminator(), Mongoose will now use the discriminator base schema's options by default. This means you don't need to explicitly set child schema options to match the base schema's.

const baseSchema = Schema({}, { typeKey: '$type' });
const Base = db.model('Base', baseSchema);

// In Mongoose 6.x, the `Base.discriminator()` call would throw because
// no `typeKey` option. In Mongoose 7, Mongoose uses the base schema's
// `typeKey` by default.
const childSchema = new Schema({}, {});
const Test = Base.discriminator('Child', childSchema);

Test.schema.options.typeKey; // '$type'

Removed castForQueryWrapper, updated castForQuery() signature

Mongoose now always calls SchemaType castForQuery() method with 3 arguments: $conditional, value, and context. If you've implemented a custom schema type that defines its own castForQuery() method, you need to update the method as follows.

// Mongoose 6.x format:
MySchemaType.prototype.castForQuery = function($conditional, value) {
  if (arguments.length === 2) {
    // Handle casting value with `$conditional` - $eq, $in, $not, etc.
  } else {
    value = $conditional;
    // Handle casting `value` with no conditional
  }
};

// Mongoose 7.x format
MySchemaType.prototype.castForQuery = function($conditional, value, context) {
  if ($conditional != null) {
    // Handle casting value with `$conditional` - $eq, $in, $not, etc.
  } else {
    // Handle casting `value` with no conditional
  }
};

Copy Schema options in Schema.prototype.add()

Mongoose now copies user defined schema options when adding one schema to another. For example, childSchema below will get baseSchema's id and toJSON options.

const baseSchema = new Schema({ created: Date }, { id: true, toJSON: { virtuals: true } });
const childSchema = new Schema([baseSchema, { name: String }]);

childSchema.options.toJSON; // { virtuals: true } in Mongoose 7. undefined in Mongoose 6.

This applies both when creating a new schema using an array of schemas, as well as when calling add() as follows.

childSchema.add(new Schema({}, { toObject: { virtuals: true } }));

childSchema.options.toObject; // { virtuals: true } in Mongoose 7. undefined in Mongoose 6.

ObjectId bsontype now has lowercase d

The internal _bsontype property on ObjectIds is equal to 'ObjectId' in Mongoose 7, as opposed to 'ObjectID' in Mongoose 6.

const oid = new mongoose.Types.ObjectId();

oid._bsontype; // 'ObjectId' in Mongoose 7, 'ObjectID' in older versions of Mongoose

Please update any places where you use _bsontype to check if an object is an ObjectId. This may also affect libraries that use Mongoose.

Removed mapReduce

MongoDB no longer supports mapReduce, so Mongoose 7 no longer has a Model.mapReduce() function. Use the aggregation framework as a replacement for mapReduce().

// The following no longer works in Mongoose 7.
const o = {
  map: function() {
    emit(this.author, 1);
  },
  reduce: function(k, vals) {
    return vals.length;
  }
};

await MR.mapReduce(o);

Removed Support for custom promise libraries

Mongoose 7 no longer supports plugging in custom promise libraries. So the following no longer makes Mongoose return Bluebird promises in Mongoose 7.

const mongoose = require('mongoose');

// No-op on Mongoose 7
mongoose.Promise = require('bluebird');

If you want to use Bluebird for all promises globally, you can do the following:

global.Promise = require('bluebird');

TypeScript-specific Changes

Removed LeanDocument and support for extends Document

Mongoose 7 no longer exports a LeanDocument type, and no longer supports passing a document type that extends Document into Model<>.

// No longer supported
interface ITest extends Document {
  name?: string;
}
const Test = model<ITest>('Test', schema);

// Do this instead, no `extends Document`
interface ITest {
  name?: string;
}
const Test = model<ITest>('Test', schema);

// If you need to access the hydrated document type, use the following code
type TestDocument = ReturnType<(typeof Test)['hydrate']>;

New Parameters for HydratedDocument

Mongoose's HydratedDocument type transforms a raw document interface into the type of the hydrated Mongoose document, including virtuals, methods, etc. In Mongoose 7, the generic parameters to HydratedDocument have changed. In Mongoose 6, the generic parameters were:

type HydratedDocument<
  DocType,
  TMethodsAndOverrides = {},
  TVirtuals = {}
> = Document<unknown, any, DocType> &
Require_id<DocType> &
TMethodsAndOverrides &
TVirtuals;

In Mongoose 7, the new type is as follows.

type HydratedDocument<
  DocType,
  TOverrides = {},
  TQueryHelpers = {}
> = Document<unknown, TQueryHelpers, DocType> &
Require_id<DocType> &
TOverrides;

In Mongoose 7, the first parameter is the raw document interface, the 2nd parameter is any document-specific overrides (usually virtuals and methods), and the 3rd parameter is any query helpers associated with the document's model.

The key difference is that, in Mongoose 6, the 3rd generic param was the document's virtuals. In Mongoose 7, the 3rd generic param is the document's query helpers.

// Mongoose 6 version:
type UserDocument = HydratedDocument<TUser, TUserMethods, TUserVirtuals>;

// Mongoose 7:
type UserDocument = HydratedDocument<TUser, TUserMethods & TUserVirtuals, TUserQueryHelpers>;