Mongoose 5.1.0 was released on May 10, and introduced 10 new features. The feature I'm most excited about is the new Map type. One of the reasons why this feature is noteworthy is because it closed out what was the oldest open issue on Mongoose's GitHub repo, originally posted in January 2012. In this article, I'll demonstrate how to use the map type and highlight why it is so important.

Introducing Mongoose Maps

Before Mongoose 5.1.0, you had to declare every property that you wanted mongoose to track and validate in your schema ahead of time. For example, let's say you had a UserSchema and wanted to store the user's Twitter, GitHub, Instagram, and other social media accounts in a socialHandles property. With a map type, this is simple, you don't have to list out each social media service explicitly.

const userSchema = new Schema({ socialHandles: { type: Map , of : String } });

Without the map type, you have 3 alternatives:

List out every social media service you want to support in your schema. This is the best option, but the list may grow out of control depending on how quickly you add new services. Plus, what if you wanted to add validation to each property?

const userSchema = new Schema({ socialHandles: { twitter: String , github: String , instagram: String , } });

Make socialHandles a mixed type property. This works, but mongoose will no longer handle casting or validation, so you're responsible for making sure each value in socialHandles is a string.

const userSchema = new Schema({ socialHandles: {} });

Set strict to false for your whole schema. This approach is theoretically possible, but not a good alternative because mongoose won't cast or validate any additional properties in socialHandles .

const userSchema = new Schema({ socialHandles: { twitter: String , github: String , instagram: String , } }, { strict: false });

With maps, you get the best of both worlds: mongoose will ensure every value in socialHandles is a string for you, and you don't have to list out every single social media service you support. Plus, you can declare validation for all your properties in one place.

const mongoose = require ( 'mongoose' ); mongoose.connect( 'mongodb://localhost:27017/test' ); const userSchema = new mongoose.Schema({ socialHandles: { type: Map , of : String } }); const User = mongoose.model( 'User' , userSchema); const doc = new User({ socialHandles: { github: 'vkarpov15' , twitter: '@code_barbarian' , instagram: 'vkarpov15' } }); console .log(doc);

Working With Maps

Mongoose maps inherit from native JavaScript's Map type, so you get the same functionality as JavaScript maps with some extra mongoose syntactic sugar baked in. For example, you can get a list of the map's keys and values using the keys() and values() methods:

const doc = new User({ socialHandles: { github: 'vkarpov15' , twitter: '@code_barbarian' , instagram: 'vkarpov15' } }); for ( const key of doc.socialHandles.keys()) { console .log(key); } for ( const val of doc.socialHandles.values()) { console .log(val); }

Because mongoose maps inherit from native JavaScript maps, if you want to get/set an individual key in the map, you need to use the get() and set() methods. Mongoose documents have get() and set() functions that you can use instead of get() and set() on the mongoose map as well. However, conventional assignment, like doc.socialHandles.github = 'vkarpov15' , does not work.

console .log(doc.socialHandles.github); console .log(doc.socialHandles.get( 'github' )); console .log(doc.get( 'socialHandles.github' )); doc.socialHandles.stackOverflow = 'vkarpov15' ; console .log(doc.get( 'socialHandles.stackOverflow' )); doc.socialHandles.set( 'stackOverflow' , 'vkarpov15' ); doc.set( 'socialHandles.stackOverflow' , 'vkarpov15' ); console .log(doc.get( 'socialHandles.stackOverflow' ));

Map Validation

Mongoose provides two mechanisms for validating your map. You can validate the whole map, or you can validate individual values in the map. Here's how you would add a custom validator to the socialHandles map.

const userSchema = new mongoose.Schema({ socialHandles: { type: Map , of : String , validate: function ( map ) { for ( const handle of map.values()) { if (handle.startsWith( 'http://' )) { throw new Error ( `Handle ${handle} must not be a URL` ); } } return true ; } } }); const User = mongoose.model( 'User' , userSchema);

With the custom validator in place, the below code will print an error.

const doc = new User({ socialHandles: { github: 'http://github.com/vkarpov15' , twitter: '@code_barbarian' , instagram: 'vkarpov15' } }); console .log(doc.validateSync().message);

The other option is to add a custom validator for the individual values in the map as opposed to the map as a whole. You do this by specifying a nested validate property in the of property.

const userSchema = new mongoose.Schema({ socialHandles: { type: Map , of : { type: String , validate: function ( str ) { if (str.startsWith( 'http://' )) { throw new Error ( `Handle ${handle} must not be a URL` ); } return true ; } } } }); const User = mongoose.model( 'User' , userSchema);

By putting a custom validator on the individual value, you'll get an error with a slightly different error message:

const doc = new User({ socialHandles: { github: 'http://github.com/vkarpov15' , twitter: '@code_barbarian' , instagram: 'vkarpov15' } }); console .log(doc.validateSync().message);

The $* in socialHandles.$* is a special placeholder specifically for map types. socialHandles.$* is the path you use to access the SchemaType of the map's values. For example, below is another way to add a custom validator to the map's values using the validate() function on SchemaType .

const userSchema = new mongoose.Schema({ socialHandles: { type: Map , of : String } }); console .log(userSchema.path( 'socialHandles' ).constructor.name); console .log(userSchema.path( 'socialHandles.$*' ).constructor.name); userSchema.path( 'socialHandles.$*' ).validate( function ( str ) { if (str.startsWith( 'http://' )) { throw new Error ( `Handle ${handle} must not be a URL` ); } return true ; });

Moving On