Mongoose 8 was released on October 31, 2023. This major release is relatively small compared to Mongoose 6, with only 14 backwards breaking changes. The driving force for a new major release just 7 months after Mongoose 7 is the 6.0 release of the MongoDB Node.js driver: we ship a new major version of Mongoose whenever the MongoDB Node.js driver ships a new major version. We also took the opportunity to remove some deprecated functions from Mongoose 7: most notably, Mongoose 8 no longer supports count() or findOneAndRemove(). In this blog post, I'll highlight some other noteworthy changes in Mongoose 8.

Mongoose's version support policy hasn't changed significantly with Mongoose 8. We will still continue to ship bug fixes and features to Mongoose 7, as well as Mongoose 8. Mongoose 5 and Mongoose 6 are still supported, but we will not backport any bug fixes or features to Mongoose 5 or 6 unless we are specifically requested to. Mongoose 5 is scheduled for end-of-life on March 1, 2024; we will not ship any fixes to Mongoose 5 after that date.

Without further ado, here's some of the highlights from Mongoose 8. You can find the full migration guide on the Mongoose docs.

Minimize on Update

Mongoose's minimize option tells Mongoose to recursively remove empty objects when saving to MongoDB. For example, suppose you have a Test model as follows:

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

// Mongoose will remove the `nested` property before storing the new document
const { _id } = await Test.create({ nested: {} });

If you load the raw document from MongoDB using lean(), you'll see that nested is undefined, not an empty object.

let rawDoc = await Test.findById(_id).lean();
rawDoc.nested; // undefined

However, in Mongoose 7, if you set a nested property to an empty object on an existing document, Mongoose won't minimize out that nested property. For the sake of making the API consistent, we made Mongoose 8 also minimize out empty nested properties when saving an existing document.

const doc = await Test.findById(_id);
doc.nested = {};
doc.markModified('nested');
await doc.save();

rawDoc = await Test.findById(_id).lean();
// In Mongoose 7, `rawDoc.nested` will be an empty object `{}`
// In Mongoose 8, `rawDoc.nested` will be `undefined`
rawDoc.nested;

If this change is problematic for you, you can either disable minimize on your schema:

const schema = new Schema({
  nested: {
    field1: Number
  }
}, {
  minimize: false // Disable minimize for this schema
});

Base Schema Validators Run Before Discriminator Schema Validators

In Mongoose 7, discriminator schemas were added to the base schema; that means base schema keys came after discriminator schema keys in key order, so all validators on base schema keys ran before validators on discriminator schema keys.

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

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

Mongoose 8 flips the order that name and otherProp are defined in the D model's final merged schema. In Mongoose 8, name will show up before otherProp in instances of D, and "Base schema validator" will print before "Discriminator schema validator".

const doc = new D({ name: 'foo', otherProp: 'bar' });

// Mongoose puts keys in schema order.
// In Mongoose 7, prints `name` after `otherProp`
// In Mongoose 8, prints `otherProp` after `name`
console.log(doc);

// In Mongoose 7, prints "Discriminator schema validator" before "Base schema validator"
// In Mongoose 8, prints "Discriminator schema validator" after "Base schema validator"
await doc.validate();

Putting discriminator properties after base schema properties is more intuitive, and helps in cases where discriminator schema setters rely on base schema setters.

String Enums Allow null

As a general rule of thumb, Mongoose treats null and undefined (so-called nullish values) as interchangeable. Before Mongoose 8, there was one big exception to this rule: null wasn't allowed as a value for string paths with an enum, even if the path wasn't required.

const schema = new Schema({
  status: {
    type: String,
    enum: ['active', 'disabled']
  }
});

const Test = mongoose.model('Test', schema);

const doc = new Test({ status: null });
// In Mongoose 8, the following `validate()` call succeeds
// In Mongoose 7, the following `validate()` throws a validation error
await doc.validate();

This behavior was inconsistent for a couple of reasons. First, while null wasn't allowed, undefined was.

const doc = new Test({ status: undefined });
// Succeeds in both Mongoose 7 and Mongoose 8
await doc.validate();

Second, number enums allowed null, string enums didn't.

const schema = new Schema({
  myNum: {
    type: Number,
    enum: [1, 2, 3]
  }
});

const Test = mongoose.model('Test', schema);

const doc = new Test({ myNum: null });
// Succeeds in both Mongoose 7 and Mongoose 8
await doc.validate();

In Mongoose 8, null is valid for string enums, unless the path is also required.

const schema = new Schema({
  status: {
    type: String,
    enum: ['active', 'disabled'],
    required: true // Disallow `null` and `undefined`
  }
});

const Test = mongoose.model('Test', schema);

const doc = new Test({ status: null });
// Throws a ValidationError in both Mongoose 7 and Mongoose 8
await doc.validate();

Moving On

Mongoose 8 is a relatively small release, but we think the changes make Mongoose's API more consistent and streamlined. Check out the migration guide for a full list of all the potentially breaking changes. Please upgrade and open any issues you find on Mongoose's GitHub page!

Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus