ObjectIds in Mongoose

Sep 3, 2019

By default, MongoDB creates an _id property on every document that's of type ObjectId. Many other databases use a numeric id property by default, but in MongoDB and Mongoose, ids are objects by default.

const Model = mongoose.model('Test', mongoose.Schema({ name: String }));
const doc = new Model({ name: 'test' });

doc._id instanceof mongoose.Types.ObjectId; // true
typeof doc._id; // 'object'
doc._id; // '5d6ede6a0ba62570afcedd3a'

Casting

MongoDB ObjectIds are typically represented using a 24 hexadecimal character string, like '5d6ede6a0ba62570afcedd3a'. Mongoose casts 24 char strings to ObjectIds for you based on your schema paths.

const schema = mongoose.Schema({ testId: mongoose.ObjectId });
const Model = mongoose.model('Test', schema);

const doc = new Model({ testId: '5d6ede6a0ba62570afcedd3a' });

// `testId` is an ObjectId, Mongoose casts 24 hex char strings to
// ObjectIds for you automatically based on your schema.
doc.testId instanceof mongoose.Types.ObjectId; // true

There are several other values that Mongoose can cast to ObjectIds. The key lesson is that an ObjectId is 12 arbitrary bytes. Any 12 byte buffer or 12 character string is a valid ObjectId.

const schema = mongoose.Schema({ testId: mongoose.ObjectId });
const Model = mongoose.model('Test', schema);

// Any 12 character string is a valid ObjectId, because the only defining
// feature of ObjectIds is that they have 12 bytes.
let doc = new Model({ testId: '12char12char' });
doc.testId instanceof mongoose.Types.ObjectId; // true
doc.testId; // '313263686172313263686172'

// Similarly, Mongoose will automatically convert buffers of length 12
// to ObjectIds.
doc = new Model({ testId: Buffer.from('12char12char') });
doc.testId instanceof mongoose.Types.ObjectId; // true
doc.testId; // '313263686172313263686172'

Getting the Timestamp from an ObjectId

ObjectIds encode the local time at which they were created. That means you can usually pull the time that a document was created from its _id.

const schema = mongoose.Schema({ testId: mongoose.ObjectId });
const Model = mongoose.model('Test', schema);

const doc = new Model({ testId: '313263686172313263686172' });
doc.testId.getTimestamp(); // '1996-02-27T01:50:32.000Z'
doc.testId.getTimestamp() instanceof Date; // true

Why ObjectIds?

Suppose you're building your own database, and you want to set a numeric id property on each new document. The id property should be increasing, so the first document you insert gets id = 0, then id = 1, and so on.

Incrementing a counter is an easy problem in a single process. But what if you have multiple processes, like a sharded cluster? Now each process needs to be able to increment the counter, so whenever you insert a document you also need to increment a distributed counter. That can lead to unreliable performance if there's significant network latency between two processes, or unpredictable results if one process is down.

ObjectIds are designed to work around this problem. ObjectId conflicts are highly unlikely, so MongoDB can assign ids that are probably unique in a distributed system with no inter-process communication.


Want to become your team's MongoDB expert? "Mastering Mongoose" distills 8 years of hard-earned lessons building Mongoose apps at scale into 153 pages. That means you can learn what you need to know to build production-ready full-stack apps with Node.js and MongoDB in a few days. Get your copy!

Did you find this tutorial useful? Say thanks by starring our repo on GitHub!

More Mongoose Tutorials