File: README.md
Mongoose
Mongoose is a MongoDB object modeling tool designed to work in an asynchronous environment. Mongoose supports Node.js and Deno (alpha).
Documentation
The official documentation website is mongoosejs.com.
Mongoose 9.0.0 was released on November 21, 2025. You can find more details on backwards breaking changes in 9.0.0 on our docs site.
Support
Plugins
Check out the plugins search site to see hundreds of related modules from the community. Next, learn how to write your own plugin from the docs or this blog post.
Contributors
Pull requests are always welcome! Please base pull requests against the master
branch and follow the contributing guide.
If your pull request makes documentation changes, please do not
modify any .html files. The .html files are compiled code, so please make
your changes in docs/*.pug, lib/*.js, or test/docs/*.js.
View all 400+ contributors.
Installation
First install Node.js and MongoDB, then install the mongoose package using your preferred package manager:
Using npm
npm install mongooseUsing pnpm
pnpm add mongooseUsing Yarn
yarn add mongooseUsing Bun
bun add mongooseMongoose 6.8.0 also includes alpha support for Deno.
Importing
// Using Node.js `require()`
const mongoose = require('mongoose');
// Using ES6 imports
import mongoose from 'mongoose';Or, using Deno's createRequire() for CommonJS support as follows.
import { createRequire } from 'https://deno.land/[email protected]/node/module.ts';
const require = createRequire(import.meta.url);
const mongoose = require('mongoose');
mongoose.connect('mongodb://127.0.0.1:27017/test')
.then(() => console.log('Connected!'));You can then run the above script using the following.
deno run --allow-net --allow-read --allow-sys --allow-env mongoose-test.jsMongoose Studio
Mongoose Studio is a free, fully open-source, browser-based MongoDB GUI built by the Mongoose team for apps that already use Mongoose. Install @mongoosejs/studio from npm and run it alongside your app as Express middleware, or deploy it on Vercel or Netlify, to browse and edit documents, query with your existing models and schemas with autocomplete, build dashboards, visualize and edit GeoJSON, and use AI-assisted MongoDB workflows without moving data into a hosted third-party workspace or sharing raw MongoDB connection strings.
Mongoose for Enterprise
Available as part of the Tidelift Subscription
The maintainers of mongoose and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. Learn more.
Overview
Connecting to MongoDB
First, we need to define a connection. If your app uses only one database, you should use mongoose.connect. If you need to create additional connections, use mongoose.createConnection.
Both connect and createConnection take a mongodb:// URI, or the parameters host, database, port, options.
await mongoose.connect('mongodb://127.0.0.1/my_database');Once connected, the open event is fired on the Connection instance. If you're using mongoose.connect, the Connection is mongoose.connection. Otherwise, mongoose.createConnection return value is a Connection.
Note: If the local connection fails then try using 127.0.0.1 instead of localhost. Sometimes issues may arise when the local hostname has been changed.
Important! Mongoose buffers all the commands until it's connected to the database. This means that you don't have to wait until it connects to MongoDB in order to define models, run queries, etc.
Defining a Model
Models are defined through the Schema interface.
const Schema = mongoose.Schema;
const ObjectId = Schema.ObjectId;
const BlogPost = new Schema({
author: ObjectId,
title: String,
body: String,
date: Date
});Aside from defining the structure of your documents and the types of data you're storing, a Schema handles the definition of:
- Validators (async and sync)
- Defaults
- Getters
- Setters
- Indexes
- Middleware
- Methods definition
- Statics definition
- Plugins
- pseudo-JOINs
The following example shows some of these features:
const Comment = new Schema({
name: { type: String, default: 'hahaha' },
age: { type: Number, min: 18, index: true },
bio: { type: String, match: /[a-z]/ },
date: { type: Date, default: Date.now },
buff: Buffer
});
// a setter
Comment.path('name').set(function(v) {
return capitalize(v);
});
// middleware
Comment.pre('save', function(next) {
notify(this.get('email'));
next();
});Accessing a Model
Once we define a model through mongoose.model('ModelName', mySchema), we can access it through the same function
const MyModel = mongoose.model('ModelName');Or just do it all at once
const MyModel = mongoose.model('ModelName', mySchema);The first argument is the singular name of the collection your model is for. Mongoose automatically looks for the plural version of your model name. For example, if you use
const MyModel = mongoose.model('Ticket', mySchema);Then MyModel will use the tickets collection, not the ticket collection. For more details read the model docs.
Once we have our model, we can then instantiate it, and save it:
const instance = new MyModel();
instance.my.key = 'hello';
await instance.save();Or we can find documents from the same collection
await MyModel.find({});You can also findOne, findById, update, etc.
const instance = await MyModel.findOne({ /* ... */ });
console.log(instance.my.key); // 'hello'For more details check out the docs.
Important! If you opened a separate connection using mongoose.createConnection() but attempt to access the model through mongoose.model('ModelName') it will not work as expected since it is not hooked up to an active db connection. In this case access your model through the connection you created:
const conn = mongoose.createConnection('your connection string');
const MyModel = conn.model('ModelName', schema);
const m = new MyModel();
await m.save(); // worksvs
const conn = mongoose.createConnection('your connection string');
const MyModel = mongoose.model('ModelName', schema);
const m = new MyModel();
await m.save(); // does not work b/c the default connection object was never connectedEmbedded Documents
In the first example snippet, we defined a key in the Schema that looks like:
comments: [Comment]Where Comment is a Schema we created. This means that creating embedded documents is as simple as:
// retrieve my model
const BlogPost = mongoose.model('BlogPost');
// create a blog post
const post = new BlogPost();
// create a comment
post.comments.push({ title: 'My comment' });
await post.save();The same goes for removing them:
const post = await BlogPost.findById(myId);
post.comments[0].deleteOne();
await post.save();Embedded documents enjoy all the same features as your models. Defaults, validators, middleware.
Middleware
See the docs page.
Intercepting and mutating method arguments
You can intercept method arguments via middleware.
For example, this would allow you to broadcast changes about your Documents every time someone sets a path in your Document to a new value:
schema.pre('set', function(next, path, val, typel) {
// `this` is the current Document
this.emit('set', path, val);
// Pass control to the next pre
next();
});Moreover, you can mutate the incoming method arguments so that subsequent middleware see different values for those arguments. To do so, just pass the new values to next:
schema.pre(method, function firstPre(next, methodArg1, methodArg2) {
// Mutate methodArg1
next('altered-' + methodArg1.toString(), methodArg2);
});
// pre declaration is chainable
schema.pre(method, function secondPre(next, methodArg1, methodArg2) {
console.log(methodArg1);
// => 'altered-originalValOfMethodArg1'
console.log(methodArg2);
// => 'originalValOfMethodArg2'
// Passing no arguments to `next` automatically passes along the current argument values
// i.e., the following `next()` is equivalent to `next(methodArg1, methodArg2)`
// and also equivalent to, with the example method arg
// values, `next('altered-originalValOfMethodArg1', 'originalValOfMethodArg2')`
next();
});Schema gotcha
type, when used in a schema has special meaning within Mongoose. If your schema requires using type as a nested property you must use object notation:
new Schema({
broken: { type: Boolean },
asset: {
name: String,
type: String // uh oh, it broke. asset will be interpreted as String
}
});
new Schema({
works: { type: Boolean },
asset: {
name: String,
type: { type: String } // works. asset is an object with a type property
}
});Driver Access
Mongoose is built on top of the official MongoDB Node.js driver. Each mongoose model keeps a reference to a native MongoDB driver collection. The collection object can be accessed using YourModel.collection. However, using the collection object directly bypasses all mongoose features, including hooks, validation, etc. The one
notable exception is that YourModel.collection still buffers
commands. As such, YourModel.collection.find() will not
return a cursor.
API Docs
Mongoose API documentation, generated using dox
and acquit.
Related Projects
MongoDB Runners
Unofficial CLIs
Data Seeding
Express Session Stores
License
Copyright (c) 2010 LearnBoost <[email protected]>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
File: docs/tutorials/custom-casting.md
Custom Casting
Mongoose 5.4.0 introduced several ways to configure SchemaTypes globally.
One of these new features is the SchemaType.cast() function, which enables you to override Mongoose's built-in casting.
For example, by default Mongoose will throw an error if you attempt to cast
a string that contains a Japanese numeral to a number.
const schema = new mongoose.Schema({
age: Number
});
const Model = mongoose.model('Test', schema);
const doc = new Model({ age: 'ไบ' });
const err = doc.validateSync();
// "Cast to Number failed for value "ไบ" at path "age""
err.message;You can overwrite the default casting function for numbers to allow converting
the string that contains the Japanese numeral "2" to a number as shown below.
// Calling `cast()` on a class that inherits from `SchemaType` returns the
// current casting function.
const originalCast = mongoose.Number.cast();
// Calling `cast()` with a function sets the current function used to
// cast a given schema type, in this cast Numbers.
mongoose.Number.cast(v => {
if (v === 'ไบ') {
return 2;
}
return originalCast(v);
});
const schema = new mongoose.Schema({
age: Number
});
const Model = mongoose.model('Test', schema);
const doc = new Model({ age: 'ไบ' });
const err = doc.validateSync();
err; // null
doc.age; // 2File: docs/tutorials/dates.md
Working With Dates
Here's how you declare a path of type Date with a Mongoose schema:
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: String,
// `lastActiveAt` is a date
lastActiveAt: Date
});
const User = mongoose.model('User', userSchema);When you create a user document, Mongoose will cast
the value to a native JavaScript date
using the Date() constructor.
const user = new User({
name: 'Jean-Luc Picard',
lastActiveAt: '2002-12-09'
});
user.lastActiveAt instanceof Date; // trueAn invalid date will lead to a CastError when you validate the document.
const user = new User({
name: 'Jean-Luc Picard',
lastActiveAt: 'not a date'
});
user.lastActiveAt instanceof Date; // false
user.validateSync().errors['lastActiveAt']; // CastErrorValidators
Dates have two built-in validators: min and max. These validators will
report a ValidatorError if the given date is strictly less than min or
strictly greater than max.
const episodeSchema = new mongoose.Schema({
title: String,
airedAt: {
type: Date,
// The dates of the first and last episodes of
// Star Trek: The Next Generation
min: '1987-09-28',
max: '1994-05-23'
}
});
const Episode = mongoose.model('Episode', episodeSchema);
const ok = new Episode({
title: 'Encounter at Farpoint',
airedAt: '1987-09-28'
});
ok.validateSync(); // No error
const bad = new Episode({
title: 'What You Leave Behind',
airedAt: '1999-06-02'
});
bad.airedAt; // "1999-06-02T00:00:00.000Z"
// Path `airedAt` (Tue Jun 01 1999 20:00:00 GMT-0400 (EDT)) is after
// maximum allowed value (Sun May 22 1994 20:00:00 GMT-0400 (EDT)).
bad.validateSync();Querying
MongoDB supports querying by date ranges and sorting by dates. Here's some
examples of querying by dates, date ranges, and sorting by date:
// Find episodes that aired on this exact date
const episodes = await Episode.find({ airedAt: new Date('1987-10-26') });
episodes[0].title; // "Where No One Has Gone Before"
// Find episodes within a range of dates, sorted by date ascending
const sortedEpisodes = await Episode.
find({ airedAt: { $gte: '1987-10-19', $lte: '1987-10-26' } }).
sort({ airedAt: 1 });
sortedEpisodes[0].title; // "The Last Outpost"
sortedEpisodes[1].title; // "Where No One Has Gone Before"Casting Edge Cases
Date casting has a couple small cases where it differs from JavaScript's
native date parsing. First, Mongoose looks for a valueOf() function on the given object,
and calls valueOf() before casting the date. This means Mongoose can cast
moment objects to dates automatically.
const moment = require('moment');
const user = new User({
name: 'Jean-Luc Picard',
lastActiveAt: moment.utc('2002-12-09')
});
user.lastActiveAt; // "2002-12-09T00:00:00.000Z"By default, if you pass a numeric
string to the Date constructor, JavaScript will attempt to convert it to a
year.
new Date(1552261496289); // "2019-03-10T23:44:56.289Z"
new Date('1552261496289'); // "Invalid Date"
new Date('2010'); // 2010-01-01T00:00:00.000ZMongoose converts numeric strings that contain numbers outside the range of representable dates in JavaScript and converts them to numbers before passing them to the date constructor.
const user = new User({
name: 'Jean-Luc Picard',
lastActiveAt: '1552261496289'
});
user.lastActiveAt; // "2019-03-10T23:44:56.289Z"Timezones
MongoDB stores dates as 64-bit integers, which
means that Mongoose does not store timezone information by default. When
you call Date#toString(), the JavaScript runtime will use your OS' timezone.
File: docs/tutorials/findoneandupdate.md
How to Use `findOneAndUpdate()` in Mongoose
The findOneAndUpdate() function in Mongoose has a wide variety of use cases. You should use save() to update documents where possible, for better validation and middleware support.
However, there are some cases where you need to use findOneAndUpdate(). In this tutorial, you'll see how to use findOneAndUpdate(), and learn when you need to use it.
- Getting Started
- Undefined Values in Updates
- Atomic Updates
- Upsert
- The
includeResultMetadataOption - Updating Discriminator Keys
Getting Started
As the name implies, findOneAndUpdate() finds the first document that matches a given filter, applies an update, and returns the document.
The findOneAndUpdate() function has the following signature:
function findOneAndUpdate(filter, update, options) {}By default, findOneAndUpdate() returns the document as it was before update was applied.
In the following example, doc initially only has name and _id properties.findOneAndUpdate() adds an age property, but the result of findOneAndUpdate() does not have an age property.
const Character = mongoose.model('Character', new mongoose.Schema({
name: String,
age: Number
}));
const _id = new mongoose.Types.ObjectId('0'.repeat(24));
let doc = await Character.create({ _id, name: 'Jean-Luc Picard' });
doc; // { name: 'Jean-Luc Picard', _id: ObjectId('000000000000000000000000') }
const filter = { name: 'Jean-Luc Picard' };
const update = { age: 59 };
// The result of `findOneAndUpdate()` is the document _before_ `update` was applied
doc = await Character.findOneAndUpdate(filter, update);
doc; // { name: 'Jean-Luc Picard', _id: ObjectId('000000000000000000000000') }
doc = await Character.findOne(filter);
doc.age; // 59You should set the returnDocument option to 'after' to return the document after update was applied.
const filter = { name: 'Jean-Luc Picard' };
const update = { age: 59 };
// `doc` is the document _after_ `update` was applied because of
// `returnDocument: 'after'`
const doc = await Character.findOneAndUpdate(filter, update, {
returnDocument: 'after'
});
doc.name; // 'Jean-Luc Picard'
doc.age; // 59Mongoose's findOneAndUpdate() is slightly different from the MongoDB Node.js driver's findOneAndUpdate() because it returns the document itself, not a result object.
The new and returnOriginal options are deprecated in favor of returnDocument.
Use returnDocument: 'after' instead of new: true or returnOriginal: false.
Use returnDocument: 'before' instead of new: false or returnOriginal: true.
Undefined Values in Updates
Mongoose removes undefined values from updates.
For example, if you set name to undefined in the following findOneAndUpdate() call, Mongoose removes name from $set before sending the update to MongoDB.
That means name remains unchanged.
Use $unset if you want to remove a property from the document.
const filter = { name: 'Jean-Luc Picard' };
const update = {
$set: {
name: undefined,
age: 59
}
};
const doc = await Character.findOneAndUpdate(filter, update, {
returnDocument: 'after'
});
doc.name; // 'Jean-Luc Picard'
doc.age; // 59Atomic Updates
With the exception of an unindexed upsert, findOneAndUpdate() is atomic. That means you can assume the document doesn't change between when MongoDB finds the document and when it updates the document, unless you're doing an upsert.
For example, if you're using save() to update a document, the document can change in MongoDB in between when you load the document using findOne() and when you save the document using save() as show below. For many use cases, the save() race condition is a non-issue. But you can work around it with findOneAndUpdate() (or transactions) if you need to.
const filter = { name: 'Jean-Luc Picard' };
const update = { age: 59 };
let doc = await Character.findOne({ name: 'Jean-Luc Picard' });
// Document changed in MongoDB, but not in Mongoose
await Character.updateOne(filter, { name: 'Will Riker' });
// This will update `doc` age to `59`, even though the doc changed.
doc.age = update.age;
await doc.save();
doc = await Character.findOne();
doc.name; // Will Riker
doc.age; // 59Upsert
Using the upsert option, you can use findOneAndUpdate() as a find-and-upsert operation. An upsert behaves like a normal findOneAndUpdate() if it finds a document that matches filter. But, if no document matches filter, MongoDB will insert one by combining filter and update as shown below.
const filter = { name: 'Will Riker' };
const update = { age: 29 };
await Character.countDocuments(filter); // 0
const doc = await Character.findOneAndUpdate(filter, update, {
new: true,
upsert: true // Make this update into an upsert
});
doc.name; // Will Riker
doc.age; // 29The `includeResultMetadata` Option
Mongoose transforms the result of findOneAndUpdate() by default: it
returns the updated document. That makes it difficult to check whether
a document was upserted or not. In order to get the updated document
and check whether MongoDB upserted a new document in the same operation,
you can set the includeResultMetadata flag to make Mongoose return the raw result
from MongoDB.
const filter = { name: 'Will Riker' };
const update = { age: 29 };
await Character.countDocuments(filter); // 0
const res = await Character.findOneAndUpdate(filter, update, {
new: true,
upsert: true,
// Return additional properties about the operation, not just the document
includeResultMetadata: true
});
res.value instanceof Character; // true
// The below property will be `false` if MongoDB upserted a new
// document, and `true` if MongoDB updated an existing object.
res.lastErrorObject.updatedExisting; // falseHere's what the res object from the above example looks like:
{ lastErrorObject:
{ n: 1,
updatedExisting: false,
upserted: 5e6a9e5ec6e44398ae2ac16a },
value:
{ _id: 5e6a9e5ec6e44398ae2ac16a,
name: 'Will Riker',
__v: 0,
age: 29 },
ok: 1 }Updating Discriminator Keys
Mongoose prevents updating the discriminator key using findOneAndUpdate() by default.
For example, suppose you have the following discriminator models.
const eventSchema = new mongoose.Schema({ time: Date });
const Event = db.model('Event', eventSchema);
const ClickedLinkEvent = Event.discriminator(
'ClickedLink',
new mongoose.Schema({ url: String })
);
const SignedUpEvent = Event.discriminator(
'SignedUp',
new mongoose.Schema({ username: String })
);Mongoose will remove __t (the default discriminator key) from the update parameter, if __t is set.
This is to prevent unintentional updates to the discriminator key; for example, if you're passing untrusted user input to the update parameter.
However, you can tell Mongoose to allow updating the discriminator key by setting the overwriteDiscriminatorKey option to true as shown below.
let event = new ClickedLinkEvent({ time: Date.now(), url: 'google.com' });
await event.save();
event = await ClickedLinkEvent.findByIdAndUpdate(
event._id,
{ __t: 'SignedUp' },
{ overwriteDiscriminatorKey: true, new: true }
);
event.__t; // 'SignedUp', updated discriminator keyFile: docs/tutorials/getters-setters.md
Getters/Setters in Mongoose
Mongoose getters and setters allow you to execute custom logic when getting or setting a property on a Mongoose document. Getters let you transform data in MongoDB into a more user friendly form, and setters let you transform user data before it gets to MongoDB.
Getters
Suppose you have a User collection and you want to obfuscate user emails to protect your users' privacy. Below is a basic userSchema that obfuscates the user's email address.
const userSchema = new Schema({
email: {
type: String,
get: obfuscate
}
});
// Mongoose passes the raw value in MongoDB `email` to the getter
function obfuscate(email) {
const separatorIndex = email.indexOf('@');
if (separatorIndex < 3) {
// '[email protected]' -> '**@gmail.com'
return email.slice(0, separatorIndex).replace(/./g, '*') +
email.slice(separatorIndex);
}
// '[email protected]' -> 'te****@gmail.com'
return email.slice(0, 2) +
email.slice(2, separatorIndex).replace(/./g, '*') +
email.slice(separatorIndex);
}
const User = mongoose.model('User', userSchema);
const user = new User({ email: '[email protected]' });
user.email; // **@gmail.comKeep in mind that getters do not impact the underlying data stored in
MongoDB. If you save user, the email property will be '[email protected]' in
the database.
By default, Mongoose does not execute getters when converting a document to JSON, including Express' res.json() function.
app.get(function(req, res) {
return User.findOne().
// The `email` getter will NOT run here
then(doc => res.json(doc)).
catch(err => res.status(500).json({ message: err.message }));
});To run getters when converting a document to JSON, set the toJSON.getters option to true in your schema as shown below.
const userSchema = new Schema({
email: {
type: String,
get: obfuscate
}
}, { toJSON: { getters: true } });
// Or, globally
mongoose.set('toJSON', { getters: true });
// Or, on a one-off basis
app.get(function(req, res) {
return User.findOne().
// The `email` getter will run here
then(doc => res.json(doc.toJSON({ getters: true }))).
catch(err => res.status(500).json({ message: err.message }));
});To skip getters on a one-off basis, use user.get() with the getters option set to false as shown below.
user.get('email', null, { getters: false }); // '[email protected]'Setters
Suppose you want to make sure all user emails in your database are lowercased to
make it easy to search without worrying about case. Below is an exampleuserSchema that ensures emails are lowercased.
const userSchema = new Schema({
email: {
type: String,
set: v => v.toLowerCase()
}
});
const User = mongoose.model('User', userSchema);
const user = new User({ email: '[email protected]' });
user.email; // '[email protected]'
// The raw value of `email` is lowercased
user.get('email', null, { getters: false }); // '[email protected]'
user.set({ email: '[email protected]' });
user.email; // '[email protected]'Mongoose also runs setters on update operations, like updateOne(). Mongoose will
upsert a document with a
lowercased email in the below example.
await User.updateOne({}, { email: '[email protected]' }, { upsert: true });
const doc = await User.findOne();
doc.email; // '[email protected]'In a setter function, this can be either the document being set or the query
being run. If you don't want your setter to run when you call updateOne(),
you add an if statement that checks if this is a Mongoose document as shown
below.
const userSchema = new Schema({
email: {
type: String,
set: toLower
}
});
function toLower(email) {
// Don't transform `email` if using `updateOne()` or `updateMany()`
if (!(this instanceof mongoose.Document)) {
return email;
}
return email.toLowerCase();
}
const User = mongoose.model('User', userSchema);
await User.updateOne({}, { email: '[email protected]' }, { upsert: true });
const doc = await User.findOne();
doc.email; // '[email protected]'Passing Parameters using `$locals`
You can't pass parameters to your getter and setter functions like you do to normal function calls.
To configure or pass additional properties to your getters and setters, you can use the document's $locals property.
The $locals property is the preferred place to store any program-defined data on your document without conflicting with schema-defined properties.
In your getter and setter functions, this is the document being accessed, so you set properties on $locals and then access those properties in your getters examples.
For example, the following shows how you can use $locals to configure the language for a custom getter that returns a string in different languages.
const internationalizedStringSchema = new Schema({
en: String,
es: String
});
const ingredientSchema = new Schema({
// Instead of setting `name` to just a string, set `name` to a map
// of language codes to strings.
name: {
type: internationalizedStringSchema,
// When you access `name`, pull the document's locale
get: function(value) {
return value[this.$locals.language || 'en'];
}
}
});
const recipeSchema = new Schema({
ingredients: [{ type: mongoose.ObjectId, ref: 'Ingredient' }]
});
const Ingredient = mongoose.model('Ingredient', ingredientSchema);
const Recipe = mongoose.model('Recipe', recipeSchema);
// Create some sample data
const { _id } = await Ingredient.create({
name: {
en: 'Eggs',
es: 'Huevos'
}
});
await Recipe.create({ ingredients: [_id] });
// Populate with setting `$locals.language` for internationalization
const language = 'es';
const recipes = await Recipe.find().populate({
path: 'ingredients',
transform: function(doc) {
doc.$locals.language = language;
return doc;
}
});
// Gets the ingredient's name in Spanish `name.es`
assert.equal(recipes[0].ingredients[0].name, 'Huevos'); // 'Huevos'Differences vs ES6 Getters/Setters
Mongoose setters are different from ES6 setters because they allow you to transform the value being set. With ES6 setters, you
would need to store an internal _email property to use a setter. With Mongoose,
you do not need to define an internal _email property or define a
corresponding getter for email.
class User {
// This won't convert the email to lowercase! That's because `email`
// is just a setter, the actual `email` property doesn't store any data.
// also eslint will warn about using "return" on a setter
set email(v) {
// eslint-disable-next-line no-setter-return
return v.toLowerCase();
}
}
const user = new User();
user.email = '[email protected]';
user.email; // undefinedFile: docs/tutorials/lean.md
Faster Mongoose Queries With Lean
The lean option tells Mongoose to skip
hydrating the result documents. This
makes queries faster and less memory intensive, but the result documents are
plain old JavaScript objects (POJOs), not Mongoose documents.
In this tutorial, you'll learn more about the tradeoffs of using lean().
Using Lean
By default, Mongoose queries return an instance of the
Mongoose Document class. Documents are much
heavier than vanilla JavaScript objects, because they have a lot of internal
state for change tracking. Enabling the lean option tells Mongoose to skip
instantiating a full Mongoose document and just give you the POJO.
const leanDoc = await MyModel.findOne().lean();How much smaller are lean documents? Here's a comparison.
const schema = new mongoose.Schema({ name: String });
const MyModel = mongoose.model('Test', schema);
await MyModel.create({ name: 'test' });
const normalDoc = await MyModel.findOne();
// To enable the `lean` option for a query, use the `lean()` function.
const leanDoc = await MyModel.findOne().lean();
v8Serialize(normalDoc).length; // approximately 180
v8Serialize(leanDoc).length; // approximately 55, about 3x smaller!
// In case you were wondering, the JSON form of a Mongoose doc is the same
// as the POJO. This additional memory only affects how much memory your
// Node.js process uses, not how much data is sent over the network.
JSON.stringify(normalDoc).length === JSON.stringify(leanDoc).length; // trueUnder the hood, after executing a query, Mongoose converts the query results
from POJOs to Mongoose documents. If you turn on the lean option, Mongoose
skips this step.
const normalDoc = await MyModel.findOne();
const leanDoc = await MyModel.findOne().lean();
normalDoc instanceof mongoose.Document; // true
normalDoc.constructor.name; // 'model'
leanDoc instanceof mongoose.Document; // false
leanDoc.constructor.name; // 'Object'The downside of enabling lean is that lean docs don't have:
- Change tracking
- Casting and validation
- Getters and setters
- Virtuals
save()
For example, the following code sample shows that the Person model's getters
and virtuals don't run if you enable lean.
// Define a `Person` model. Schema has 2 custom getters and a `fullName`
// virtual. Neither the getters nor the virtuals will run if lean is enabled.
const personSchema = new mongoose.Schema({
firstName: {
type: String,
get: capitalizeFirstLetter
},
lastName: {
type: String,
get: capitalizeFirstLetter
}
});
personSchema.virtual('fullName').get(function() {
return `${this.firstName} ${this.lastName}`;
});
function capitalizeFirstLetter(v) {
// Convert 'bob' -> 'Bob'
return v.charAt(0).toUpperCase() + v.substring(1);
}
const Person = mongoose.model('Person', personSchema);
// Create a doc and load it as a lean doc
await Person.create({ firstName: 'benjamin', lastName: 'sisko' });
const normalDoc = await Person.findOne();
const leanDoc = await Person.findOne().lean();
normalDoc.fullName; // 'Benjamin Sisko'
normalDoc.firstName; // 'Benjamin', because of `capitalizeFirstLetter()`
normalDoc.lastName; // 'Sisko', because of `capitalizeFirstLetter()`
leanDoc.fullName; // undefined
leanDoc.firstName; // 'benjamin', custom getter doesn't run
leanDoc.lastName; // 'sisko', custom getter doesn't runLean and Populate
Populate works with lean(). If you
use both populate() and lean(), the lean option propagates to the
populated documents as well. In the below example, both the top-level
'Group' documents and the populated 'Person' documents will be lean.
// Create models
const Group = mongoose.model('Group', new mongoose.Schema({
name: String,
members: [{ type: mongoose.ObjectId, ref: 'Person' }]
}));
const Person = mongoose.model('Person', new mongoose.Schema({
name: String
}));
// Initialize data
const people = await Person.create([
{ name: 'Benjamin Sisko' },
{ name: 'Kira Nerys' }
]);
await Group.create({
name: 'Star Trek: Deep Space Nine Characters',
members: people.map(p => p._id)
});
// Execute a lean query
const group = await Group.findOne().lean().populate('members');
group.members[0].name; // 'Benjamin Sisko'
group.members[1].name; // 'Kira Nerys'
// Both the `group` and the populated `members` are lean.
group instanceof mongoose.Document; // false
group.members[0] instanceof mongoose.Document; // false
group.members[1] instanceof mongoose.Document; // falseVirtual populate also works with lean.
// Create models
const groupSchema = new mongoose.Schema({ name: String });
groupSchema.virtual('members', {
ref: 'Person',
localField: '_id',
foreignField: 'groupId'
});
const Group = mongoose.model('Group', groupSchema);
const Person = mongoose.model('Person', new mongoose.Schema({
name: String,
groupId: mongoose.ObjectId
}));
// Initialize data
const g = await Group.create({ name: 'DS9 Characters' });
await Person.create([
{ name: 'Benjamin Sisko', groupId: g._id },
{ name: 'Kira Nerys', groupId: g._id }
]);
// Execute a lean query
const group = await Group.findOne().lean().populate({
path: 'members',
options: { sort: { name: 1 } }
});
group.members[0].name; // 'Benjamin Sisko'
group.members[1].name; // 'Kira Nerys'
// Both the `group` and the populated `members` are lean.
group instanceof mongoose.Document; // false
group.members[0] instanceof mongoose.Document; // false
group.members[1] instanceof mongoose.Document; // falseWhen to Use Lean
If you're executing a query and sending the results without modification to,
say, an Express response, you should
use lean. In general, if you do not modify the query results and do not use
custom getters, you should uselean(). If you modify the query results or rely on features like getters
or transforms, you should not
use lean().
Below is an example of an Express route
that is a good candidate for lean(). This route does not modify the person
doc and doesn't rely on any Mongoose-specific functionality.
// As long as you don't need any of the Person model's virtuals or getters,
// you can use `lean()`.
app.get('/person/:id', function(req, res) {
Person.findOne({ _id: req.params.id }).lean().
then(person => res.json({ person })).
catch(error => res.json({ error: error.message }));
});Below is an example of an Express route that should not use lean(). As
a general rule of thumb, GET routes are good candidates for lean() in a
RESTful API.
On the other hand, PUT, POST, etc. routes generally should not use lean().
// This route should **not** use `lean()`, because lean means no `save()`.
app.put('/person/:id', function(req, res) {
Person.findOne({ _id: req.params.id }).
then(person => {
assert.ok(person);
Object.assign(person, req.body);
return person.save();
}).
then(person => res.json({ person })).
catch(error => res.json({ error: error.message }));
});Remember that virtuals do not end up in lean() query results. Use the
mongoose-lean-virtuals plugin
to add virtuals to your lean query results.
Plugins
Using lean() bypasses all Mongoose features, including virtuals, getters/setters,
and defaults. If you want to
use these features with lean(), you need to use the corresponding plugin:
However, you need to keep in mind that Mongoose does not hydrate lean documents,
so this will be a POJO in virtuals, getters, and default functions.
const schema = new Schema({ name: String });
schema.plugin(require('mongoose-lean-virtuals'));
schema.virtual('lowercase', function() {
this instanceof mongoose.Document; // false
this.name; // Works
this.get('name'); // Crashes because `this` is not a Mongoose document.
});BigInts
By default, the MongoDB Node driver converts longs stored in MongoDB into JavaScript numbers, not BigInts.
Set the useBigInt64 option on your lean() queries to inflate longs into BigInts.
const Person = mongoose.model('Person', new mongoose.Schema({
name: String,
age: BigInt
}));
// Mongoose will convert `age` to a BigInt
const { age } = await Person.create({ name: 'Benjamin Sisko', age: 37 });
typeof age; // 'bigint'
// By default, if you store a document with a BigInt property in MongoDB and you
// load the document with `lean()`, the BigInt property will be a number
let person = await Person.findOne({ name: 'Benjamin Sisko' }).lean();
typeof person.age; // 'number'
// Set the `useBigInt64` option to opt in to converting MongoDB longs to BigInts.
person = await Person.findOne({ name: 'Benjamin Sisko' }).
setOptions({ useBigInt64: true }).
lean();
typeof person.age; // 'bigint'File: docs/tutorials/query_casting.md
Query Casting
The first parameter to Model.find(), Query#find(), Model.findOne(), etc. is called filter.
In older content this parameter is sometimes called query or conditions. For example:
const query = Character.find({ name: 'Jean-Luc Picard' });
query.getFilter(); // `{ name: 'Jean-Luc Picard' }`
// Subsequent chained calls merge new properties into the filter
query.find({ age: { $gt: 50 } });
query.getFilter(); // `{ name: 'Jean-Luc Picard', age: { $gt: 50 } }`When you execute the query using Query#exec() or Query#then(), Mongoose casts the filter to match your schema.
// Note that `_id` and `age` are strings. Mongoose will cast `_id` to
// a MongoDB ObjectId and `age.$gt` to a number.
const query = Character.findOne({
_id: '5cdc267dd56b5662b7b7cc0c',
age: { $gt: '50' }
});
// `{ _id: '5cdc267dd56b5662b7b7cc0c', age: { $gt: '50' } }`
// Query hasn't been executed yet, so Mongoose hasn't casted the filter.
query.getFilter();
const doc = await query.exec();
doc.name; // "Jean-Luc Picard"
// Mongoose casted the filter, so `_id` became an ObjectId and `age.$gt`
// became a number.
query.getFilter()._id instanceof mongoose.Types.ObjectId; // true
typeof query.getFilter().age.$gt === 'number'; // trueIf Mongoose fails to cast the filter to your schema, your query will throw a CastError.
const query = Character.findOne({ age: { $lt: 'not a number' } });
const err = await query.exec().then(() => null, err => err);
err instanceof mongoose.CastError; // true
// Cast to number failed for value "not a number" at path "age" for
// model "Character"
err.message;The `strictQuery` Option
By default, Mongoose does not cast filter properties that aren't in your schema.
const query = Character.findOne({ notInSchema: { $lt: 'not a number' } });
// No error because `notInSchema` is not defined in the schema
await query.exec();You can configure this behavior using the strictQuery option for schemas. This option is analogous to the strict option. Setting strictQuery to true removes non-schema properties from the filter:
mongoose.deleteModel('Character');
const schema = new mongoose.Schema({ name: String, age: Number }, {
strictQuery: true
});
Character = mongoose.model('Character', schema);
const query = Character.findOne({ notInSchema: { $lt: 'not a number' } });
await query.exec();
query.getFilter(); // Empty object `{}`, Mongoose removes `notInSchema`To make Mongoose throw an error if your filter has a property that isn't in the schema, set strictQuery to 'throw':
mongoose.deleteModel('Character');
const schema = new mongoose.Schema({ name: String, age: Number }, {
strictQuery: 'throw'
});
Character = mongoose.model('Character', schema);
const query = Character.findOne({ notInSchema: { $lt: 'not a number' } });
const err = await query.exec().then(() => null, err => err);
err.name; // 'StrictModeError'
// Path "notInSchema" is not in schema and strictQuery is 'throw'.
err.message;Implicit `$in`
Because of schemas, Mongoose knows what types fields should be, so it can provide some neat syntactic sugar. For example, if you forget to put $in on a non-array field, Mongoose will add $in for you.
// Normally wouldn't find anything because `name` is a string, but
// Mongoose automatically inserts `$in`
const query = Character.findOne({ name: ['Jean-Luc Picard', 'Will Riker'] });
const doc = await query.exec();
doc.name; // "Jean-Luc Picard"
// `{ name: { $in: ['Jean-Luc Picard', 'Will Riker'] } }`
query.getFilter();File: docs/tutorials/ssl.md
TLS/SSL Connections
Mongoose supports connecting to MongoDB clusters that require TLS/SSL connections. Setting the tls option to true in mongoose.connect() or your connection string is enough to connect to a MongoDB cluster using TLS/SSL:
mongoose.connect('mongodb://127.0.0.1:27017/test', { tls: true });
// Equivalent:
mongoose.connect('mongodb://127.0.0.1:27017/test?tls=true');The tls option defaults to false for connection strings that start with mongodb://. However,
the tls option defaults to true for connection strings that start with mongodb+srv://. So if you are using an srv connection string to connect to MongoDB Atlas, TLS/SSL is enabled by default.
If you try to connect to a MongoDB cluster that requires TLS/SSL without enabling the tls/ssl option, mongoose.connect() will error out with the below error:
MongooseServerSelectionError: connection <monitor> to 127.0.0.1:27017 closed
at NativeConnection.Connection.openUri (/node_modules/mongoose/lib/connection.js:800:32)
...TLS/SSL Validation
By default, Mongoose validates the TLS/SSL certificate against a certificate authority to ensure the TLS/SSL certificate is valid. To disable this validation, set the tlsAllowInvalidCertificates (or tlsInsecure) option to true.
mongoose.connect('mongodb://127.0.0.1:27017/test', {
tls: true,
tlsAllowInvalidCertificates: true,
});In most cases, you should not disable TLS/SSL validation in production. However, tlsAllowInvalidCertificates: true is often helpful
for debugging SSL connection issues. If you can connect to MongoDB with tlsAllowInvalidCertificates: true, but not withtlsAllowInvalidCertificates: false, then you can confirm Mongoose can connect to the server and the server is configured to use
TLS/SSL correctly, but there's some issue with the certificate.
For example, a common issue is the below error message:
MongooseServerSelectionError: unable to verify the first certificateThis error is often caused by self-signed MongoDB certificates or other situations where the certificate sent by the MongoDB
server is not registered with an established certificate authority. The solution is to set the tlsCAFile option, which essentially sets a list of allowed SSL certificates.
await mongoose.connect('mongodb://127.0.0.1:27017/test', {
tls: true,
// For example, see https://medium.com/@rajanmaharjan/secure-your-mongodb-connections-ssl-tls-92e2addb3c89
// for where the `rootCA.pem` file comes from.
tlsCAFile: `${__dirname}/rootCA.pem`,
});Another common issue is the below error message:
MongooseServerSelectionError: Hostname/IP does not match certificate's altnames: Host: hostname1. is not cert's CN: hostname2The SSL certificate's common name must line up with the host name
in your connection string. If the SSL certificate is for hostname2.mydomain.com, your connection string must connect to hostname2.mydomain.com, not any other hostname or IP address that may be equivalent to hostname2.mydomain.com. For replica sets, this also means that the SSL certificate's common name must line up with the machine's hostname. To disable this validation, set the tlsAllowInvalidHostnames option to true.
X.509 Authentication
If you're using X.509 authentication, you should set the user name in the connection string, not the connect() options.
// Do this:
const username = 'myusername';
await mongoose.connect(`mongodb://${encodeURIComponent(username)}@127.0.0.1:27017/test`, {
tls: true,
tlsCAFile: `${__dirname}/rootCA.pem`,
authMechanism: 'MONGODB-X509',
});
// Not this:
await mongoose.connect('mongodb://127.0.0.1:27017/test', {
tls: true,
tlsCAFile: `${__dirname}/rootCA.pem`,
authMechanism: 'MONGODB-X509',
auth: { username },
});X.509 Authentication with MongoDB Atlas
With MongoDB Atlas, X.509 certificates are not Root CA certificates and will not work with the tlsCAFile parameter as self-signed certificates would. If the tlsCAFile parameter is used an error similar to the following would be raised:
MongoServerSelectionError: unable to get local issuer certificateTo connect to a MongoDB Atlas cluster using X.509 authentication the correct option to set is tlsCertificateKeyFile. The connection string already specifies the authSource and authMechanism, however they're included below as connect() options for completeness:
const url = 'mongodb+srv://xyz.mongodb.net/test?authSource=%24external&authMechanism=MONGODB-X509';
await mongoose.connect(url, {
tls: true,
// location of a local .pem file that contains both the client's certificate and key
tlsCertificateKeyFile: '/path/to/certificate.pem',
authMechanism: 'MONGODB-X509',
authSource: '$external',
});Note The connection string options must be URL escaped correctly.
File: docs/tutorials/virtuals.md
Mongoose Virtuals
In Mongoose, a virtual is a property that is not stored in MongoDB.
Virtuals are typically used for computed properties on documents.
- Your First Virtual
- Virtual Setters
- Virtuals in JSON
- Virtuals with Lean
- Limitations
- Populate
- Virtuals via schema options
- Further Reading
Your First Virtual
Suppose you have a User model. Every user has an email, but you also
want the email's domain. For example, the domain portion of
'[email protected]' is 'gmail.com'.
Below is one way to implement the domain property using a virtual.
You define virtuals on a schema using the Schema#virtual() function.
const userSchema = mongoose.Schema({
email: String
});
// Create a virtual property `domain` that's computed from `email`.
userSchema.virtual('domain').get(function() {
return this.email.slice(this.email.indexOf('@') + 1);
});
const User = mongoose.model('User', userSchema);
const doc = await User.create({ email: '[email protected]' });
// `domain` is now a property on User documents.
doc.domain; // 'gmail.com'The Schema#virtual() function returns a VirtualType object. Unlike normal document properties,
virtuals do not have any underlying value and Mongoose does not do
any type coercion on virtuals. However, virtuals do have
getters and setters, which make
them ideal for computed properties, like the domain example above.
Virtual Setters
You can also use virtuals to set multiple properties at once as an
alternative to custom setters on normal properties. For example, suppose
you have two string properties: firstName and lastName. You can
create a virtual property fullName that lets you set both of
these properties at once. The key detail is that, in virtual getters and
setters, this refers to the document the virtual is attached to.
const userSchema = mongoose.Schema({
firstName: String,
lastName: String
});
// Create a virtual property `fullName` with a getter and setter.
userSchema.virtual('fullName').
get(function() { return `${this.firstName} ${this.lastName}`; }).
set(function(v) {
// `v` is the value being set, so use the value to set
// `firstName` and `lastName`.
const firstName = v.substring(0, v.indexOf(' '));
const lastName = v.substring(v.indexOf(' ') + 1);
this.set({ firstName, lastName });
});
const User = mongoose.model('User', userSchema);
const doc = new User();
// Vanilla JavaScript assignment triggers the setter
doc.fullName = 'Jean-Luc Picard';
doc.fullName; // 'Jean-Luc Picard'
doc.firstName; // 'Jean-Luc'
doc.lastName; // 'Picard'Virtuals in JSON
By default, Mongoose does not include virtuals when you convert a document to JSON.
For example, if you pass a document to Express' res.json() function, virtuals will not be included by default.
To include virtuals in res.json(), you need to set thetoJSON schema option to { virtuals: true }.
const opts = { toJSON: { virtuals: true } };
const userSchema = mongoose.Schema({
_id: Number,
email: String
}, opts);
// Create a virtual property `domain` that's computed from `email`.
userSchema.virtual('domain').get(function() {
return this.email.slice(this.email.indexOf('@') + 1);
});
const User = mongoose.model('User', userSchema);
const doc = new User({ _id: 1, email: '[email protected]' });
doc.toJSON().domain; // 'gmail.com'
// {"_id":1,"email":"[email protected]","domain":"gmail.com","id":"1"}
JSON.stringify(doc);
// To skip applying virtuals, pass `virtuals: false` to `toJSON()`
doc.toJSON({ virtuals: false }).domain; // undefinedVirtuals in `console.log()`
By default, Mongoose does not include virtuals in console.log() output.
To include virtuals in console.log(), you need to set the toObject schema option to { virtuals: true }, or use toObject() before printing the object.
console.log(doc.toObject({ virtuals: true }));Virtuals with Lean
Virtuals are properties on Mongoose documents. If you use the
lean option, that means your queries return POJOs
rather than full Mongoose documents. That means no virtuals if you uselean().
const fullDoc = await User.findOne();
fullDoc.domain; // 'gmail.com'
const leanDoc = await User.findOne().lean();
leanDoc.domain; // undefinedIf you use lean() for performance, but still need virtuals, Mongoose
has an
officially supported mongoose-lean-virtuals plugin
that decorates lean documents with virtuals.
Limitations
Mongoose virtuals are not stored in MongoDB, which means you can't query
based on Mongoose virtuals.
// Will **not** find any results, because `domain` is not stored in
// MongoDB.
const doc = await User.findOne({ domain: 'gmail.com' }, null, { strictQuery: false });
doc; // undefinedIf you want to query by a computed property, you should set the property using
a custom setter or pre save middleware.
Populate
Mongoose also supports populating virtuals. A populated
virtual contains documents from another collection. To define a populated
virtual, you need to specify:
- The
refoption, which tells Mongoose which model to populate documents from. - The
localFieldandforeignFieldoptions. Mongoose will populate documents from the model inrefwhoseforeignFieldmatches this document'slocalField.
const userSchema = mongoose.Schema({ _id: Number, email: String });
const blogPostSchema = mongoose.Schema({
title: String,
authorId: Number
});
// When you `populate()` the `author` virtual, Mongoose will find the
// first document in the User model whose `_id` matches this document's
// `authorId` property.
blogPostSchema.virtual('author', {
ref: 'User',
localField: 'authorId',
foreignField: '_id',
justOne: true
});
const User = mongoose.model('User', userSchema);
const BlogPost = mongoose.model('BlogPost', blogPostSchema);
await BlogPost.create({ title: 'Introduction to Mongoose', authorId: 1 });
await User.create({ _id: 1, email: '[email protected]' });
const doc = await BlogPost.findOne().populate('author');
doc.author.email; // '[email protected]'Virtuals via schema options
Virtuals can also be defined in the schema-options directly without having to use .virtual:
const userSchema = mongoose.Schema({
firstName: String,
lastName: String
}, {
virtuals: {
// Create a virtual property `fullName` with a getter and setter
fullName: {
get() { return `${this.firstName} ${this.lastName}`; },
set(v) {
// `v` is the value being set, so use the value to set
// `firstName` and `lastName`.
const firstName = v.substring(0, v.indexOf(' '));
const lastName = v.substring(v.indexOf(' ') + 1);
this.set({ firstName, lastName });
}
}
}
});
const User = mongoose.model('User', userSchema);
const doc = new User();
// Vanilla JavaScript assignment triggers the setter
doc.fullName = 'Jean-Luc Picard';
doc.fullName; // 'Jean-Luc Picard'
doc.firstName; // 'Jean-Luc'
doc.lastName; // 'Picard'The same also goes for virtual options, like virtual populate:
const userSchema = mongoose.Schema({ _id: Number, email: String });
const blogPostSchema = mongoose.Schema({
title: String,
authorId: Number
}, {
virtuals: {
// When you `populate()` the `author` virtual, Mongoose will find the
// first document in the User model whose `_id` matches this document's
// `authorId` property.
author: {
options: {
ref: 'User',
localField: 'authorId',
foreignField: '_id',
justOne: true
}
}
}
});
const User = mongoose.model('User', userSchema);
const BlogPost = mongoose.model('BlogPost', blogPostSchema);
await BlogPost.create({ title: 'Introduction to Mongoose', authorId: 1 });
await User.create({ _id: 1, email: '[email protected]' });
const doc = await BlogPost.findOne().populate('author');
doc.author.email; // '[email protected]'Further Reading
- Virtuals in Mongoose Schemas
- Populate Virtuals
- Mongoose Lean Virtuals plugin
- Getting Started With Mongoose Virtuals
- Understanding Virtuals in Mongoose
File: docs/typescript/populate.md
Populate with TypeScript
Mongoose's TypeScript bindings add a generic parameter Paths to the populate():
import { Schema, model, Document, Types } from 'mongoose';
// `Parent` represents the object as it is stored in MongoDB
interface Parent {
child?: Types.ObjectId,
name?: string
}
const ParentModel = model('Parent', new Schema({
child: { type: Schema.Types.ObjectId, ref: 'Child' },
name: String
}));
interface Child {
name: string;
}
const childSchema = new Schema({ name: String });
const ChildModel = model<Child>('Child', childSchema);
// Populate with `Paths` generic `{ child: Child }` to override `child` path
ParentModel.findOne({}).populate<{ child: Child }>('child').orFail().then(doc => {
// Works
const t: string = doc.child.name;
});An alternative approach is to define a PopulatedParent interface and use Pick<> to pull the properties you're populating.
import { Schema, model, Document, Types } from 'mongoose';
// `Parent` represents the object as it is stored in MongoDB
interface Parent {
child?: Types.ObjectId,
name?: string
}
interface Child {
name: string;
}
interface PopulatedParent {
child: Child | null;
}
const ParentModel = model('Parent', new Schema({
child: { type: Schema.Types.ObjectId, ref: 'Child' },
name: String
}));
const childSchema = new Schema({ name: String });
const ChildModel = model<Child>('Child', childSchema);
// Populate with `Paths` generic `{ child: Child }` to override `child` path
ParentModel.findOne({}).populate>('child').orFail().then(doc => {
// Works
const t: string = doc.child.name;
});Using `PopulatedDoc`
Mongoose also exports a PopulatedDoc type that helps you define populated documents in your document interface:
import { Schema, model, Document, PopulatedDoc } from 'mongoose';
// `child` is either an ObjectId or a populated document
interface Parent {
child?: PopulatedDoc<Document<ObjectId> & Child>,
name?: string
}
const ParentModel = model('Parent', new Schema({
child: { type: 'ObjectId', ref: 'Child' },
name: String
}));
interface Child {
name?: string;
}
const childSchema = new Schema({ name: String });
const ChildModel = model<Child>('Child', childSchema);
ParentModel.findOne({}).populate('child').orFail().then((doc: Parent) => {
const child = doc.child;
if (child == null || child instanceof ObjectId) {
throw new Error('should be populated');
} else {
// Works
doc.child.name.trim();
}
});However, we recommend using the .populate<{ child: Child }> syntax from the first section instead of PopulatedDoc.
Here's two reasons why:
- You still need to add an extra check to check if
child instanceof ObjectId. Otherwise, the TypeScript compiler will fail withProperty name does not exist on type ObjectId. So usingPopulatedDoc<>means you need an extra check everywhere you usedoc.child. - In the
Parentinterface,childis a hydrated document, which makes it difficult for Mongoose to infer the type ofchildwhen you uselean()ortoObject().