Mongoose 5.4 was released on December 14, with 13 new features. The overarching theme for the most important new features is making Mongoose SchemaTypes configurable at the level of individual types. Before digging in to the new features, let's first review what a SchemaType is.

What Are SchemaTypes?

In Mongoose, a schema defines the shape of a document: what properties the document should have, and what types those properties should be. In Mongoose, A schema contains zero or more SchemaType instances, and each SchemaType instance defines what type a single property should be. For example:

const mongoose = require('mongoose');

// `schema` will have one SchemaType with `path = 'name'`
const schema = new mongoose.Schema({
  name: String
}, { _id: false });

schema.path('name') instanceof mongoose.SchemaType; // true
schema.path('name') instanceof mongoose.Schema.Types.String; // true

// SchemaString {
//   enumValues: [],
//   regExp: null,
//   path: 'name',
//   instance: 'String',
//   ...
console.log(schema.path('name'));

// You can iterate over all SchemaTypes in a schema using `eachPath()`
// See https://mongoosejs.com/docs/api.html#schema_Schema-eachPath
schema.eachPath((path, schematype) => console.log(schematype.path)); // "name"

SchemaType is a class, and there are several classes that inherit from SchemaType:

  • mongoose.Schema.Types.String
  • mongoose.Schema.Types.Number or, equivalently, mongoose.Number
  • mongoose.Schema.Types.Date
  • mongoose.Schema.Types.Buffer
  • mongoose.Schema.Types.Mixed, or, equivalently, mongoose.Mixed
  • mongoose.Schema.Types.ObjectId, or, equivalently, mongoose.ObjectId
  • mongoose.Schema.Types.Decimal128, or, equivalently, mongoose.Decimal128

When you define a path count in your schema with type: Number, Mongoose creates an instance of the mongoose.Number, and schema.path('count').path === 'count'.

Configuring SchemaType Classes

Before Mongoose 5.4, the SchemaType class had no static properties. You could add validation, getters, setters, and other custom behavior to individual SchemaType instances, but not to all SchemaType instances.

Mongoose 5.4 added several static functions to the SchemaType class: cast(), get(), and checkRequired(). These functions let you configure behavior for all instances of a SchemaType.

For example, several people have requested that Mongoose cast empty string '' to false for boolean types. With Mongoose 5.4, you can use mongoose.Boolean.cast() to wrap the logic Mongoose uses to cast booleans:

const mongoose = require('mongoose');

// Wrap Boolean casting so empty string becomes false
const original = mongoose.Schema.Types.Boolean.cast();
mongoose.Schema.Types.Boolean.cast(v => {
  if (v === '') {
    return false;
  }
  return original(v);
});

const schema = new mongoose.Schema({
  test: Boolean
}, { _id: false });

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

const doc = new Model({ test: '' });

doc.test; // false
doc.validateSync(); // undefined, no error!

You can also pass false to cast() to disable casting for a given SchemaType:

const mongoose = require('mongoose');

// Disable casting for numbers: only nullish values or values with
// `typeof v === 'number'` are allowed. Everything else causes a CastError
mongoose.Number.cast(false);

const schema = new mongoose.Schema({
  test: Number
}, { _id: false });

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

const doc = new Model({ test: '123' });

doc.validateSync(); // CastError for path `test`

SchemaType Getters

The SchemaType.get() function lets you define a custom getter for all instances of a given SchemaType. For example, many people have requested Mongoose automatically convert MongoDB ObjectIds to hex strings. Because a MongoDB ObjectId is an object, comparing ObjectIds using built-in JavaScript functions is cumbersome:

$ node -v
v8.9.4
$ node
> const mongoose = require('mongoose')
undefined
> const oid1 = new mongoose.Types.ObjectId()
undefined
> const oid2 = new mongoose.Types.ObjectId(oid1.toString())
undefined
> oid1
5c2e35400844102978e69f8c
> oid2
5c2e35400844102978e69f8c
> oid1 == oid2
false
> oid1.toString() === oid2.toString()
true
> [oid1].indexOf(oid2)
-1
> [oid1].includes(oid2)
false

Before Mongoose 5.4, you could define a custom getter on each individual ObjectId path that would convert ObjectIds to strings for you:

const assert = require('assert');
const mongoose = require('mongoose');

const schema = new mongoose.Schema({
  test: {
    type: mongoose.ObjectId,
    get: v => v.toString() // Convert `test` to a string when you do `doc.test`
  }
}, { _id: false });

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

const doc = new Model({ test: '5c2e35400844102978e69f8c' });

// `doc.test` will always be a string
assert.ok(typeof doc.test === 'string');
assert.ok(doc.test === '5c2e35400844102978e69f8c');

However, you would have to add a getter to every ObjectId path. With SchemaType.get(), you can add the above getter to every ObjectId path using the below one-liner.

mongoose.ObjectId.get(v => v.toString());

Here's an example of comparing ObjectIds using === with a global getter.

const assert = require('assert');
const mongoose = require('mongoose');

mongoose.ObjectId.get(v => v.toString());

const schema = new mongoose.Schema({
  test: mongoose.ObjectId
}, { _id: false });

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

const doc = new Model({ test: '5c2e35400844102978e69f8c' });

// `doc.test` will always be a string
assert.ok(typeof doc.test === 'string');
assert.ok(doc.test === '5c2e35400844102978e69f8c');

const doc2 = new Model({ test: doc.test.toString() });

// So you can compare ObjectIds using `===`
assert.ok(doc.test === doc2.test);

const doc3 = new Model({ test: 'bad' });
assert.ok(doc3.validateSync() instanceof Error); // Mongoose still casts

Moving On

Global getters and custom casting are just 2 of 13 new features in Mongoose 5.4. There's also Model.findOneAndReplace(), a count option for populate virtuals, and a Query#map() function. Make sure you upgrade and take advantage of these new features!

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