An Introduction to Mongoose Arrays

May 22, 2020

Mongoose's Array class extends vanilla JavaScript arrays with additional Mongoose functionality. For example, suppose you have a blog post schema with an array of tags.

const blogPostSchema = Schema({
  title: String,
  tags: [String]
});

When you create a new BlogPost document, the tags property is an instance of the vanilla JavaScript array class. But it also has some special properties.

const blogPostSchema = Schema({
  title: String,
  tags: [String]
}, { versionKey: false });
const BlogPost = mongoose.model('BlogPost', blogPostSchema);

const doc = new BlogPost({
  title: 'Intro to JavaScript',
  tags: ['programming']
});

Array.isArray(doc.tags); // true
doc.tags.isMongooseArray; // true

For example, Mongoose intercepts push() calls on the tags array, and is smart enough to update the document using $push when you save() the document.

mongoose.set('debug', true);

doc.tags.push('web development');
// Because of 'debug' mode, will print:
// Mongoose: blogposts.updateOne({ _id: ObjectId(...) }, { '$push': { tags: { '$each': [ 'web development' ] } } }, { session: null })
await doc.save();

Document Arrays

The tags example is an array of primitives. Mongoose also supports arrays of subdocuments. Here's how you can define an array of members, each with a firstName and lastName property.

const groupSchema = Schema({
  name: String,
  members: [{ firstName: String, lastName: String }]
});

doc.members is an instance of a vanilla JavaScript array, so it has all the usual functions, like slice() and filter(). But it also has some Mongoose-specific functionality baked in.

const groupSchema = Schema({
  name: String,
  members: [{ firstName: String, lastName: String }]
});
const Group = mongoose.model('Group', groupSchema);

const doc = new Group({
  title: 'Jedi Order',
  members: [{ firstName: 'Luke', lastName: 'Skywalker' }]
});

Array.isArray(doc.members); // true
doc.members.isMongooseArray; // true
doc.members.isMongooseDocumentArray; // true

For example, if you set the 0th member's firstName, Mongoose will translate that to a set on member.0.firstName when you call save().

const groupSchema = Schema({
  name: String,
  members: [{ firstName: String, lastName: String }]
}, { versionKey: false });
const Group = mongoose.model('Group', groupSchema);

const doc = new Group({
  title: 'Jedi Order',
  members: [{ firstName: 'Luke', lastName: 'Skywalker' }]
});
await doc.save();

mongoose.set('debug', true);

doc.members[0].firstName = 'Anakin';
// Prints:
// Mongoose: groups.updateOne({ _id: ObjectId("...") },
// { '$set': { 'members.0.firstName': 'Anakin' } }, { session: null })
await doc.save();

Caveat With Setting Array Indexes

Mongoose has a known issue with setting array indexes directly. For example, if you set doc.tags[0], Mongoose change tracking won't pick up that change.

const blogPostSchema = Schema({
  title: String,
  tags: [String]
}, { versionKey: false });
const BlogPost = mongoose.model('BlogPost', blogPostSchema);

const doc = new BlogPost({
  title: 'Intro to JavaScript',
  tags: ['programming']
});
await doc.save();

// This change won't end up in the database!
doc.tags[0] = 'JavaScript';
await doc.save();

const fromDb = await BlogPost.findOne({ _id: doc._id });
fromDb.tags; // ['programming']

To work around this caveat, you need to inform Mongoose's change tracking of the change, either using the markModified() method or by explicitly calling MongooseArray#set() on the array element as shown below.

// This change works. `set()` is a special method on Mongoose
// arrays that triggers change tracking.
doc.tags.set(0, 'JavaScript');
await doc.save();

const fromDb = await BlogPost.findOne({ _id: doc._id });
fromDb.tags; // ['JavaScript']

More Mongoose Tutorials