Migrating from 8.x to 9.x
There are several backwards-breaking changes you should be aware of when migrating from Mongoose 8.x to Mongoose 9.x.
If you're still on Mongoose 7.x or earlier, please read the Mongoose 7.x to 8.x migration guide and upgrade to Mongoose 8.x first before upgrading to Mongoose 9.
Schema.prototype.doValidate() now returns a promise
Schema.prototype.doValidate() now returns a promise that rejects with a validation error if one occurred.
In Mongoose 8.x, doValidate() took a callback and did not return a promise.
// Mongoose 8.x function signature
function doValidate(value, cb, scope, options) {}
// Mongoose 8.x example usage
schema.doValidate(value, function(error) {
if (error) {
// Handle validation error
}
}, scope, options);
// Mongoose 9.x function signature
async function doValidate(value, scope, options) {}
// Mongoose 9.x example usage
try {
await schema.doValidate(value, scope, options);
} catch (error) {
// Handle validation error
}
Errors in middleware functions take priority over next() calls
In Mongoose 8.x, if a middleware function threw an error after calling next(), that error would be ignored.
schema.pre('save', function(next) {
next();
// In Mongoose 8, this error will not get reported, because you already called next()
throw new Error('woops!');
});In Mongoose 9, errors in the middleware function take priority, so the above save() would throw an error.
next() no longer supports passing arguments to the next middleware
Previously, you could call next(null, 'new arg') in a hook and the args to the next middleware would get overwritten by 'new arg'.
schema.pre('save', function(next, options) {
options; // options passed to `save()`
next(null, 'new arg');
});
schema.pre('save', function(next, arg) {
arg; // In Mongoose 8, this would be 'new arg', overwrote the options passed to `save()`
});In Mongoose 9, next(null, 'new arg') doesn't overwrite the args to the next middleware.
Update pipelines disallowed by default
As of MongoDB 4.2, you can pass an array of pipeline stages to updateOne(), updateMany(), and findOneAndUpdate() to modify the document in multiple stages.
Mongoose does not cast update pipelines at all, so for Mongoose 9 we've made using update pipelines throw an error by default.
// Throws in Mongoose 9. Works in Mongoose 8
await Model.updateOne({}, [{ $set: { newProp: 'test2' } }]);Set updatePipeline: true to enable update pipelines.
// Works in Mongoose 9
await Model.updateOne({}, [{ $set: { newProp: 'test2' } }], { updatePipeline: true });You can also set updatePipeline globally to enable update pipelines for all update operations by default.
// Enable update pipelines globally
mongoose.set('updatePipeline', true);
// Now update pipelines work without needing to specify the option on each query
await Model.updateOne({}, [{ $set: { newProp: 'test2' } }]);
// You can still override the global setting per query
await Model.updateOne({}, [{ $set: { newProp: 'test2' } }], { updatePipeline: false }); // throwsRemoved background option for indexes
MongoDB no longer supports the background option for indexes as of MongoDB 4.2. Mongoose 9 will no longer set the background option by default and Mongoose 9 no longer supports setting the background option on Schema.prototype.index().
mongoose.isValidObjectId() returns false for numbers
In Mongoose 8, you could create a new ObjectId from a number, and isValidObjectId() would return true for numbers. In Mongoose 9, isValidObjectId() will return false for numbers and you can no longer create a new ObjectId from a number.
// true in mongoose 8, false in mongoose 9
mongoose.isValidObjectId(6);
// Works in Mongoose 8, throws in Mongoose 9
new mongoose.Types.ObjectId(6);
## Subdocument `deleteOne()` hooks execute only when subdocument is deleted
Currently, calling `deleteOne()` on a subdocument will execute the `deleteOne()` hooks on the subdocument regardless of whether the subdocument is actually deleted.
```javascript
const SubSchema = new Schema({
myValue: {
type: String
}
}, {});
let count = 0;
SubSchema.pre('deleteOne', { document: true, query: false }, function(next) {
count++;
next();
});
const schema = new Schema({
foo: {
type: String,
required: true
},
mySubdoc: {
type: [SubSchema],
required: true
}
}, { minimize: false, collection: 'test' });
const Model = db.model('TestModel', schema);
const newModel = {
foo: 'bar',
mySubdoc: [{ myValue: 'some value' }]
};
const doc = await Model.create(newModel);
// In Mongoose 8, the following would trigger the `deleteOne` hook, even if `doc` is not saved or deleted.
doc.mySubdoc[0].deleteOne();
// In Mongoose 9, you would need to either `save()` or `deleteOne()` on `doc` to trigger the subdocument `deleteOne` hook.
await doc.save();Hooks for custom methods and statics no longer support callbacks
Previously, you could use Mongoose middleware with custom methods and statics that took callbacks. In Mongoose 9, this is no longer supported. If you want to use Mongoose middleware with a custom method or static, that custom method or static must be an async function or return a Promise.
const mySchema = new Schema({
name: String
});
// This is an example of a custom method that uses callbacks. While this method by itself still works in Mongoose 9,
// Mongoose 9 no longer supports hooks for this method.
mySchema.methods.foo = async function(cb) {
return cb(null, this.name);
};
mySchema.statics.bar = async function(cb) {
return cb(null, 'bar');
};
// This is no longer supported because `foo()` and `bar()` use callbacks.
mySchema.pre('foo', function() {
console.log('foo pre hook');
});
mySchema.pre('bar', function() {
console.log('bar pre hook');
});
// The following code has a custom method and a custom static that use async functions.
// The following works correctly in Mongoose 9: `pre('bar')` is executed when you call `bar()` and
// `pre('qux')` is executed when you call `qux()`.
mySchema.methods.baz = async function baz(arg) {
return arg;
};
mySchema.pre('baz', async function baz() {
console.log('baz pre hook');
});
mySchema.statics.qux = async function qux(arg) {
return arg;
};
mySchema.pre('qux', async function qux() {
console.log('qux pre hook');
});
Document.prototype.updateOne no longer accepts a callback
Document.prototype.updateOne still supported callbacks in Mongoose 8. In Mongoose 9, the callback parameter was removed.
const doc = await TestModel.findOne().orFail();
// Worked in Mongoose 8, no longer supported in Mongoose 9.
doc.updateOne({ name: 'updated' }, null, (err, res) => {
if (err) throw err;
console.log(res);
});
Removed promiseOrCallback
Mongoose 9 removed the promiseOrCallback helper function.
const { promiseOrCallback } = require('mongoose');
promiseOrCallback; // undefined in Mongoose 9
isAsync middleware no longer supported
Mongoose 9 no longer supports isAsync middleware. Middleware functions that use the legacy signature with both next and done callbacks (i.e., function(next, done)) are not supported. We recommend middleware now use promises or async/await.
If you have code that uses isAsync middleware, you must refactor it to use async functions or return a promise instead.
// ❌ Not supported in Mongoose 9
const schema = new Schema({});
schema.pre('save', true, function(next, done) {
execed.first = true;
setTimeout(
function() {
done(new Error('first done() error'));
},
5);
next();
});
schema.pre('save', true, function(next, done) {
execed.second = true;
setTimeout(
function() {
next(new Error('second next() error'));
done(new Error('second done() error'));
},
25);
});
// ✅ Supported in Mongoose 9: use async functions or return a promise
schema.pre('save', async function() {
execed.first = true;
await new Promise(resolve => setTimeout(resolve, 5));
});
schema.pre('save', async function() {
execed.second = true;
await new Promise(resolve => setTimeout(resolve, 25));
});
Removed skipOriginalStackTraces option
In Mongoose 8, Mongoose queries store an _executionStack property that stores the stack trace of where the query was originally executed for debugging Query was already executed errors.
This behavior can cause performance issues with bundlers and source maps.
skipOriginalStackTraces was added to work around this behavior.
In Mongoose 9, this option is no longer necessary because Mongoose no longer stores the original stack trace.
Node.js version support
Mongoose 9 requires Node.js 18 or higher.
UUID's are now MongoDB UUID objects
Mongoose 9 now returns UUID objects as instances of bson.UUID. In Mongoose 8, UUIDs were Mongoose Buffers that were converted to strings via a getter.
const schema = new Schema({ uuid: 'UUID' });
const TestModel = mongoose.model('Test', schema);
const test = new TestModel({ uuid: new bson.UUID() });
await test.save();
test.uuid; // string in Mongoose 8, bson.UUID instance in Mongoose 9With this change, UUIDs will be represented in hex string format in JSON, even if getters: true is not set.
If you want to convert UUIDs to strings via a getter by default, you can use mongoose.Schema.Types.UUID.get():
// Configure all UUIDs to have a getter which converts the UUID to a string
mongoose.Schema.Types.UUID.get(v => v == null ? v : v.toString());
const schema = new Schema({ uuid: 'UUID' });
const TestModel = mongoose.model('Test', schema);
const test = new TestModel({ uuid: new bson.UUID() });
await test.save();
test.uuid; // string
SchemaType caster and casterConstructor properties were removed
In Mongoose 8, certain schema type instances had a caster property which contained either the embedded schema type or embedded subdocument constructor.
In Mongoose 9, to make types and internal logic more consistent, we removed the caster property in favor of embeddedSchemaType and Constructor.
const schema = new mongoose.Schema({ docArray: [new mongoose.Schema({ name: String })], arr: [String] });
// In Mongoose 8:
console.log(schema.path('arr').caster); // SchemaString
console.log(schema.path('docArray').caster); // EmbeddedDocument constructor
console.log(schema.path('arr').casterConstructor); // SchemaString constructor
console.log(schema.path('docArray').casterConstructor); // EmbeddedDocument constructor
// In Mongoose 9:
console.log(schema.path('arr').embeddedSchemaType); // SchemaString
console.log(schema.path('docArray').embeddedSchemaType); // SchemaDocumentArrayElement
console.log(schema.path('arr').Constructor); // undefined
console.log(schema.path('docArray').Constructor); // EmbeddedDocument constructorIn Mongoose 8, there was also an internal $embeddedSchemaType property. That property has been replaced with embeddedSchemaType, which is now part of the public API.
Removed skipId parameter to Model() and Document()
In Mongoose 8, the 3rd parameter to Model() and Document() was either a boolean or options object.
If a boolean, Mongoose would interpret the 3rd parameter as the skipId option.
In Mongoose 9, the 3rd parameter is always an options object, passing a boolean is no longer supported.
Query use$geoWithin removed, now always true
mongoose.Query had a use$geoWithin property that could configure converting $geoWithin to $within to support MongoDB versions before 2.4.
That property has been removed in Mongoose 9. $geoWithin is now never converted to $within, because MongoDB no longer supports $within.
Removed noListener option from useDb()/connections
The noListener option has been removed from connections and from the useDb() method. In Mongoose 8.x, you could call useDb() with { noListener: true } to prevent the new connection object from listening to state changes on the base connection, which was sometimes useful to reduce memory usage when dynamically creating connections for every request.
In Mongoose 9.x, the noListener option is no longer supported or documented. The second argument to useDb() now only supports { useCache }.
// Mongoose 8.x
conn.useDb('myDb', { noListener: true }); // works
// Mongoose 9.x
conn.useDb('myDb', { noListener: true }); // TypeError: noListener is not a supported option
conn.useDb('myDb', { useCache: true }); // worksTypeScript
FilterQuery renamed to QueryFilter
In Mongoose 9, FilterQuery (the first parameter to Model.find(), Model.findOne(), etc.) was renamed to QueryFilter.
QueryFilter Properties No Longer Resolve to any
In Mongoose 9, the QueryFilter type, which is the type of the first param to Model.find(), Model.findOne(), etc. now enforces stronger types for top-level keys.
const schema = new Schema({ age: Number });
const TestModel = mongoose.model('Test', schema);
TestModel.find({ age: 'not a number' }); // Works in Mongoose 8, TS error in Mongoose 9
TestModel.find({ age: { $notAnOperator: 42 } }); // Works in Mongoose 8, TS error in Mongoose 9This change is backwards breaking if you use generics when creating queries as shown in the following example.
If you run into the following issue or any similar issues, you can use as QueryFilter.
// From https://stackoverflow.com/questions/56505560/how-to-fix-ts2322-could-be-instantiated-with-a-different-subtype-of-constraint:
// "Never assign a concrete type to a generic type parameter, consider it as read-only!"
// This function is generally something you shouldn't do in TypeScript, can work around it with `as` though.
function findById<ModelType extends {_id: Types.ObjectId | string}>(model: Model<ModelType>, _id: Types.ObjectId | string) {
return model.find({_id: _id} as QueryFilter<ModelType>); // In Mongoose 8, this `as` was not required
}
No more generic parameter for create() and insertOne()
In Mongoose 8, create() and insertOne() accepted a generic parameter, which meant TypeScript let you pass any value to the function.
const schema = new Schema({ age: Number });
const TestModel = mongoose.model('Test', schema);
// Worked in Mongoose 8, TypeScript error in Mongoose 9
const doc = await TestModel.create({ age: 'not a number', someOtherProperty: 'value' });In Mongoose 9, create() and insertOne() no longer accept a generic parameter. Instead, they accept Partial<RawDocType> with some additional query casting applied that allows objects for maps, strings for ObjectIds, and POJOs for subdocuments and document arrays.
If your parameters to create() don't match Partial<RawDocType>, you can use as to cast as follows.
const doc = await TestModel.create({ age: 'not a number', someOtherProperty: 'value' } as unknown as Partial<InferSchemaType<typeof schema>>);
Document id is no longer any
In Mongoose 8 and earlier, id was a property on the Document class that was set to any.
This was inconsistent with runtime behavior, where id is a virtual property that returns _id as a string, unless there is already an id property on the schema or the schema has the id option set to false.
Mongoose 9 appends id as a string property to TVirtuals. The Document class no longer has an id property.
const schema = new Schema({ age: Number });
const TestModel = mongoose.model('Test', schema);
const doc = new TestModel();
doc.id; // 'string' in Mongoose 9, 'any' in Mongoose 8.
