Migrating from 7.x to 8.x

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

If you're still on Mongoose 6.x or earlier, please read the Mongoose 6.x to 7.x migration guide and upgrade to Mongoose 7.x first before upgrading to Mongoose 8.

We also recommend reviewing the MongoDB Node.js driver's release notes for v6.0.0 before upgrading to Mongoose 8.

Removed rawResult option for findOneAndUpdate()

The rawResult option for findOneAndUpdate(), findOneAndReplace(), and findOneAndDelete() has been replaced by the includeResultMetadata option.

const filter = { name: 'Will Riker' };
const update = { age: 29 };

const res = await Character.findOneAndUpdate(filter, update, {
  new: true,
  upsert: true,
  // Replace `rawResult: true` with `includeResultMetadata: true`
  includeResultMetadata: true
});

includeResultMetadata in Mongoose 8 behaves identically to rawResult.

Document.prototype.deleteOne now returns a query

In Mongoose 7, doc.deleteOne() returned a promise that resolved to doc. In Mongoose 8, doc.deleteOne() returns a query for easier chaining, as well as consistency with doc.updateOne().

const numberOne = await Character.findOne({ name: 'Will Riker' });

// In Mongoose 7, q is a Promise that resolves to `numberOne`
// In Mongoose 8, q is a Query.
const q = numberOne.deleteOne();

// In Mongoose 7, `res === numberOne`
// In Mongoose 8, `res` is a `DeleteResult`.
const res = await q;

MongoDB Node Driver 6

Mongoose 8 uses v6.x of the MongoDB Node driver. There's a few noteable changes in MongoDB Node driver v6 that affect Mongoose:

  1. The ObjectId constructor no longer accepts strings of length 12. In Mongoose 7, new mongoose.Types.ObjectId('12charstring') was perfectly valid. In Mongoose 8, new mongoose.Types.ObjectId('12charstring') throws an error.

  2. Deprecated SSL options have been removed

    • sslCA -> tlsCAFile
    • sslCRL -> tlsCRLFile
    • sslCert -> tlsCertificateKeyFile
    • sslKey -> tlsCertificateKeyFile
    • sslPass -> tlsCertificateKeyFilePassword
    • sslValidate -> tlsAllowInvalidCertificates
    • tlsCertificateFile -> tlsCertificateKeyFile

Removed findOneAndRemove()

In Mongoose 7, findOneAndRemove() was an alias for findOneAndDelete() that Mongoose supported for backwards compatibility. Mongoose 8 no longer supports findOneAndRemove(). Use findOneAndDelete() instead.

Removed count()

Model.count() and Query.prototype.count() were removed in Mongoose 8. Use Model.countDocuments() and Query.prototype.countDocuments() instead.

Removed id Setter

In Mongoose 7.4, Mongoose introduced an id setter that made doc.id = '0'.repeat(24) equivalent to doc._id = '0'.repeat(24). In Mongoose 8, that setter is now removed.

null is valid for non-required string enums

Before Mongoose 8, setting a string path with an enum to null would lead to a validation error, even if that path wasn't required. In Mongoose 8, it is valid to set a string path to null if required is not set, even with enum.

const schema = new Schema({
  status: {
    type: String,
    enum: ['on', 'off']
  }
});
const Test = mongoose.model('Test', schema);

// Works fine in Mongoose 8
// Throws a `ValidationError` in Mongoose 7
await Test.create({ status: null });

Apply minimize when save() updates an existing document

In Mongoose 7, Mongoose would only apply minimize when saving a new document, not when updating an existing document.

const schema = new Schema({
  nested: {
    field1: Number
  }
});
const Test = mongoose.model('Test', schema);

// Both Mongoose 7 and Mongoose 8 strip out empty objects when saving
// a new document in MongoDB by default
const { _id } = await Test.create({ nested: {} });
let rawDoc = await Test.findById(_id).lean();
rawDoc.nested; // undefined

// Mongoose 8 will also strip out empty objects when saving an
// existing document in MongoDB
const doc = await Test.findById(_id);
doc.nested = {};
doc.markModified('nested');
await doc.save();

let rawDoc = await Test.findById(_id).lean();
rawDoc.nested; // undefined in Mongoose 8, {} in Mongoose 7

Apply base schema paths before discriminator paths

This means that, in Mongoose 8, getters and setters on discriminator paths run after getters and setters on base paths. In Mongoose 7, getters and setters on discriminator paths ran before getters and setters on base paths.


const schema = new Schema({
  name: {
    type: String,
    get(v) {
      console.log('Base schema getter');
      return v;
    }
  }
});

const Test = mongoose.model('Test', schema);
const D = Test.discriminator('D', new Schema({
  otherProp: {
    type: String,
    get(v) {
      console.log('Discriminator schema getter');
      return v;
    }
  }
}));

const doc = new D({ name: 'test', otherProp: 'test' });
// In Mongoose 8, prints "Base schema getter" followed by "Discriminator schema getter"
// In Mongoose 7, prints "Discriminator schema getter" followed by "Base schema getter"
console.log(doc.toObject({ getters: true }));

Removed overwrite option for findOneAndUpdate()

Mongoose 7 and earlier supported an overwrite option for findOneAndUpdate(), updateOne(), and update(). Before Mongoose 7, overwrite would skip wrapping the update parameter in $set, which meant that findOneAndUpdate() and update() would overwrite the matched document. In Mongoose 7, setting overwrite would convert findOneAndUpdate() to findOneAndReplace() and updateOne() to replaceOne() to retain backwards compatibility.

In Mongoose 8, the overwrite option is no longer supported. If you want to overwrite the entire document, use findOneAndReplace() or replaceOne().

Changed behavior for findOneAndUpdate() with orFail() and upsert

In Mongoose 7, findOneAndUpdate(filter, update, { upsert: true }).orFail() would throw a DocumentNotFoundError if a new document was upserted. In other words, findOneAndUpdate().orFail() always threw an error if no document was found, even if a new document was upserted.

In Mongoose 8, findOneAndUpdate(filter, update, { upsert: true }).orFail() always succeeds. findOneAndUpdate().orFail() now throws a DocumentNotFoundError if there's no document returned, rather than if no document was found.

create() waits until all saves are done before throwing any error

In Mongoose 7, create() would immediately throw if any save() threw an error by default. Mongoose 8 instead waits for all save() calls to finish before throwing the first error that occurred. So create() will throw the same error in both Mongoose 7 and Mongoose 8, Mongoose 8 just may take longer to throw the error.

const schema = new Schema({
  name: {
    type: String,
    enum: ['Badger', 'Mushroom']
  }
});
schema.pre('save', async function() {
  await new Promise(resolve => setTimeout(resolve, 1000));
});
const Test = mongoose.model('Test', schema);

const err = await Test.create([
  { name: 'Badger' },
  { name: 'Mushroom' },
  { name: 'Cow' }
]).then(() => null, err => err);
err; // ValidationError

// In Mongoose 7, there would be 0 documents, because `Test.create()`
// would throw before 'Badger' and 'Mushroom' are inserted
// In Mongoose 8, there will be 2 documents. `Test.create()` waits until
// 'Badger' and 'Mushroom' are inserted before throwing.
await Test.countDocuments();

Model.validate() returns copy of object

In Mongoose 7, Model.validate() would potentially modify the passed in object. Mongoose 8 instead copies the passed in object first.

const schema = new Schema({ answer: Number });
const Test = mongoose.model('Test', schema);

const obj = { answer: '42' };
const res = Test.validate(obj);

typeof obj.answer; // 'string' in Mongoose 8, 'number' in Mongoose 7 
typeof res.answer; // 'number' in both Mongoose 7 and Mongoose 8

Allow null For Optional Fields in TypeScript

In Mongoose 8, automatically inferred schema types in TypeScript allow null for optional fields. In Mongoose 7, optional fields only allowed undefined, not null.

const schema = new Schema({ name: String });
const TestModel = model('Test', schema);

const doc = new TestModel();

// In Mongoose 8, this type is `string | null | undefined`.
// In Mongoose 7, this type is `string | undefined`
doc.name;

Model constructor properties are all optional in TypeScript

In Mongoose 8, no properties are required on model constructors by default.

import {Schema, model, Model} from 'mongoose';

interface IDocument {
  name: string;
  createdAt: Date;
  updatedAt: Date;
}

const documentSchema = new Schema<IDocument>(
  { name: { type: String, required: true } },
  { timestamps: true }
);

const TestModel = model<IDocument>('Document', documentSchema);

// Would throw a compile error in Mongoose 7, compiles in Mongoose 8
const newDoc = new TestModel({
  name: 'Foo'
});

// Explicitly pass generic param to constructor to specify the expected
// type of the model constructor param. The following will cause TS
// to complain about missing `createdAt` and `updatedAt` in Mongoose 8.
const newDoc2 = new TestModel<IDocument>({
  name: 'Foo'
});

Infer distinct() return types from schema

interface User {
  name: string;
  email: string;
  avatar?: string;
}
const schema = new Schema<User>({
  name: { type: String, required: true },
  email: { type: String, required: true },
  avatar: String
});

// Works in Mongoose 8. Compile error in Mongoose 7.
const names: string[] = await MyModel.distinct('name');