SchemaTypes handle definition of path defaults, validation, getters, setters, field selection defaults for queries, and other general characteristics for Mongoose document properties.
type
Keyschema.path()
FunctionYou can think of a Mongoose schema as the configuration object for a Mongoose model. A SchemaType is then a configuration object for an individual property. A SchemaType says what type a given path should have, whether it has any getters/setters, and what values are valid for that path.
const schema = newSchema({ name: String }); schema.path('name') instanceof mongoose.SchemaType; // true schema.path('name') instanceof mongoose.Schema.Types.String; // true schema.path('name').instance; // 'String'
A SchemaType is different from a type. In other words, mongoose.ObjectId !== mongoose.Types.ObjectId
. A SchemaType is just a configuration object for Mongoose. An instance of the mongoose.ObjectId
SchemaType doesn't actually create MongoDB ObjectIds, it is just a configuration for a path in a schema.
The following are all the valid SchemaTypes in Mongoose. Mongoose plugins can also add custom SchemaTypes like int32. Check out Mongoose's plugins search to find plugins.
const schema = newSchema({ name: String, binary: Buffer, living: Boolean, updated: { type: Date, default: Date.now }, age: { type: Number, min: 18, max: 65 }, mixed: Schema.Types.Mixed, _someId: Schema.Types.ObjectId, decimal: Schema.Types.Decimal128, double: Schema.Types.Double, int32bit: Schema.Types.Int32, array: [], ofString: [String], ofNumber: [Number], ofDates: [Date], ofBuffer: [Buffer], ofBoolean: [Boolean], ofMixed: [Schema.Types.Mixed], ofObjectId: [Schema.Types.ObjectId], ofArrays: [[]], ofArrayOfNumbers: [[Number]], nested: { stuff: { type: String, lowercase: true, trim: true } }, map: Map, mapOfString: { type: Map, of: String } }); // example useconstThing = mongoose.model('Thing', schema); const m = newThing; m.name = 'Statue of Liberty'; m.age = 125; m.updated = newDate; m.binary = Buffer.alloc(0); m.living = false; m.mixed = { any: { thing: 'i want' } }; m.markModified('mixed'); m._someId = new mongoose.Types.ObjectId; m.array.push(1); m.ofString.push('strings!'); m.ofNumber.unshift(1, 2, 3, 4); m.ofDates.addToSet(newDate); m.ofBuffer.pop(); m.ofMixed = [1, [], 'three', { four: 5 }]; m.nested.stuff = 'good'; m.map = newMap([['key', 'value']]); m.save(callback);
type
Key type
is a special property in Mongoose schemas. When Mongoose finds a nested property named type
in your schema, Mongoose assumes that it needs to define a SchemaType with the given type.
// 3 string SchemaTypes: 'name', 'nested.firstName', 'nested.lastName'const schema = newSchema({ name: { type: String }, nested: { firstName: { type: String }, lastName: { type: String } } });
As a consequence, you need a little extra work to define a property named type
in your schema. For example, suppose you're building a stock portfolio app, and you want to store the asset's type
(stock, bond, ETF, etc.). Naively, you might define your schema as shown below:
const holdingSchema = newSchema({ // You might expect `asset` to be an object that has 2 properties,// but unfortunately `type` is special in Mongoose so mongoose// interprets this schema to mean that `asset` is a stringasset: { type: String, ticker: String } });
However, when Mongoose sees type: String
, it assumes that you mean asset
should be a string, not an object with a property type
. The correct way to define an object with a property type
is shown below.
const holdingSchema = newSchema({ asset: { // Workaround to make sure Mongoose knows `asset` is an object// and `asset.type` is a string, rather than thinking `asset`// is a string.type: { type: String }, ticker: String } });
You can declare a schema type using the type directly, or an object with a type
property.
const schema1 = newSchema({ test: String// `test` is a path of type String }); const schema2 = newSchema({ // The `test` object contains the "SchemaType options"test: { type: String } // `test` is a path of type string });
In addition to the type property, you can specify additional properties for a path. For example, if you want to lowercase a string before saving:
const schema2 = newSchema({ test: { type: String, lowercase: true// Always convert `test` to lowercase } });
You can add any property you want to your SchemaType options. Many plugins rely on custom SchemaType options. For example, the mongoose-autopopulate plugin automatically populates paths if you set autopopulate: true
in your SchemaType options. Mongoose comes with support for several built-in SchemaType options, like lowercase
in the above example.
The lowercase
option only works for strings. There are certain options which apply for all schema types, and some that apply for specific schema types.
required
: boolean or function, if true adds a required validator for this propertydefault
: Any or function, sets a default value for the path. If the value is a function, the return value of the function is used as the default.select
: boolean, specifies default projections for queriesvalidate
: function, adds a validator function for this propertyget
: function, defines a custom getter for this property using Object.defineProperty()
.set
: function, defines a custom setter for this property using Object.defineProperty()
.alias
: string, mongoose >= 4.10.0 only. Defines a virtual with the given name that gets/sets this path.immutable
: boolean, defines path as immutable. Mongoose prevents you from changing immutable paths unless the parent document has isNew: true
.transform
: function, Mongoose calls this function when you call Document#toJSON()
function, including when you JSON.stringify()
a document.const numberSchema = newSchema({ integerOnly: { type: Number, get: v =>Math.round(v), set: v =>Math.round(v), alias: 'i' } }); constNumber = mongoose.model('Number', numberSchema); const doc = newNumber(); doc.integerOnly = 2.001; doc.integerOnly; // 2 doc.i; // 2 doc.i = 3.001; doc.integerOnly; // 3 doc.i; // 3
You can also define MongoDB indexes using schema type options.
index
: boolean, whether to define an index on this property.unique
: boolean, whether to define a unique index on this property.sparse
: boolean, whether to define a sparse index on this property.const schema2 = newSchema({ test: { type: String, index: true, unique: true// Unique index. If you specify `unique: true`// specifying `index: true` is optional if you do `unique: true` } });
lowercase
: boolean, whether to always call .toLowerCase()
on the valueuppercase
: boolean, whether to always call .toUpperCase()
on the valuetrim
: boolean, whether to always call .trim()
on the valuematch
: RegExp, creates a validator that checks if the value matches the given regular expressionenum
: Array, creates a validator that checks if the value is in the given array.minLength
: Number, creates a validator that checks if the value length is not less than the given numbermaxLength
: Number, creates a validator that checks if the value length is not greater than the given numberpopulate
: Object, sets default populate optionsmin
: Number, creates a validator that checks if the value is greater than or equal to the given minimum.max
: Number, creates a validator that checks if the value is less than or equal to the given maximum.enum
: Array, creates a validator that checks if the value is strictly equal to one of the values in the given array.populate
: Object, sets default populate optionsmin
: Date, creates a validator that checks if the value is greater than or equal to the given minimum.max
: Date, creates a validator that checks if the value is less than or equal to the given maximum.expires
: Number or String, creates a TTL index with the value expressed in seconds.populate
: Object, sets default populate optionsTo declare a path as a string, you may use either the String
global constructor or the string 'String'
.
const schema1 = newSchema({ name: String }); // name will be cast to stringconst schema2 = newSchema({ name: 'String' }); // EquivalentconstPerson = mongoose.model('Person', schema2);
If you pass an element that has a toString()
function, Mongoose will call it, unless the element is an array or the toString()
function is strictly equal to Object.prototype.toString()
.
newPerson({ name: 42 }).name; // "42" as a stringnewPerson({ name: { toString: () =>42 } }).name; // "42" as a string// "undefined", will get a cast error if you `save()` this documentnewPerson({ name: { foo: 42 } }).name;
To declare a path as a number, you may use either the Number
global constructor or the string 'Number'
.
const schema1 = newSchema({ age: Number }); // age will be cast to a Numberconst schema2 = newSchema({ age: 'Number' }); // EquivalentconstCar = mongoose.model('Car', schema2);
There are several types of values that will be successfully cast to a Number.
newCar({ age: '15' }).age; // 15 as a NumbernewCar({ age: true }).age; // 1 as a NumbernewCar({ age: false }).age; // 0 as a NumbernewCar({ age: { valueOf: () =>83 } }).age; // 83 as a Number
If you pass an object with a valueOf()
function that returns a Number, Mongoose will call it and assign the returned value to the path.
The values null
and undefined
are not cast.
NaN, strings that cast to NaN, arrays, and objects that don't have a valueOf()
function will all result in a CastError once validated, meaning that it will not throw on initialization, only when validated.
Built-in Date
methods are not hooked into the mongoose change tracking logic which in English means that if you use a Date
in your document and modify it with a method like setMonth()
, mongoose will be unaware of this change and doc.save()
will not persist this modification. If you must modify Date
types using built-in methods, tell mongoose about the change with doc.markModified('pathToYourDate')
before saving.
constAssignment = mongoose.model('Assignment', { dueDate: Date }); const doc = awaitAssignment.findOne(); doc.dueDate.setMonth(3); await doc.save(); // THIS DOES NOT SAVE YOUR CHANGE doc.markModified('dueDate'); await doc.save(); // works
To declare a path as a Buffer, you may use either the Buffer
global constructor or the string 'Buffer'
.
const schema1 = newSchema({ binData: Buffer }); // binData will be cast to a Bufferconst schema2 = newSchema({ binData: 'Buffer' }); // EquivalentconstData = mongoose.model('Data', schema2);
Mongoose will successfully cast the below values to buffers.
const file1 = newData({ binData: 'test'}); // {"type":"Buffer","data":[116,101,115,116]}const file2 = newData({ binData: 72987 }); // {"type":"Buffer","data":[27]}const file4 = newData({ binData: { type: 'Buffer', data: [1, 2, 3]}}); // {"type":"Buffer","data":[1,2,3]}
An "anything goes" SchemaType. Mongoose will not do any casting on mixed paths. You can define a mixed path using Schema.Types.Mixed
or by passing an empty object literal. The following are equivalent.
constAny = newSchema({ any: {} }); constAny = newSchema({ any: Object }); constAny = newSchema({ any: Schema.Types.Mixed }); constAny = newSchema({ any: mongoose.Mixed });
Since Mixed is a schema-less type, you can change the value to anything else you like, but Mongoose loses the ability to auto detect and save those changes. To tell Mongoose that the value of a Mixed type has changed, you need to call doc.markModified(path)
, passing the path to the Mixed type you just changed.
To avoid these side-effects, a Subdocument path may be used instead.
person.anything = { x: [3, 4, { y: 'changed' }] }; person.markModified('anything'); person.save(); // Mongoose will save changes to `anything`.
An ObjectId is a special type typically used for unique identifiers. Here's how you declare a schema with a path driver
that is an ObjectId:
const mongoose = require('mongoose'); const carSchema = new mongoose.Schema({ driver: mongoose.ObjectId });
ObjectId
is a class, and ObjectIds are objects. However, they are often represented as strings. When you convert an ObjectId to a string using toString()
, you get a 24-character hexadecimal string:
constCar = mongoose.model('Car', carSchema); const car = newCar(); car.driver = new mongoose.Types.ObjectId(); typeof car.driver; // 'object' car.driverinstanceof mongoose.Types.ObjectId; // true car.driver.toString(); // Something like "5e1a0651741b255ddda996c4"
Booleans in Mongoose are plain JavaScript booleans. By default, Mongoose casts the below values to true
:
true
'true'
1
'1'
'yes'
Mongoose casts the below values to false
:
false
'false'
0
'0'
'no'
Any other value causes a CastError. You can modify what values Mongoose converts to true or false using the convertToTrue
and convertToFalse
properties, which are JavaScript sets.
const M = mongoose.model('Test', newSchema({ b: Boolean })); console.log(newM({ b: 'nay' }).b); // undefined// Set { false, 'false', 0, '0', 'no' }console.log(mongoose.Schema.Types.Boolean.convertToFalse); mongoose.Schema.Types.Boolean.convertToFalse.add('nay'); console.log(newM({ b: 'nay' }).b); // false
Mongoose supports arrays of SchemaTypes and arrays of subdocuments. Arrays of SchemaTypes are also called primitive arrays, and arrays of subdocuments are also called document arrays.
constToySchema = newSchema({ name: String }); constToyBoxSchema = newSchema({ toys: [ToySchema], buffers: [Buffer], strings: [String], numbers: [Number] // ... etc });
Arrays are special because they implicitly have a default value of []
(empty array).
constToyBox = mongoose.model('ToyBox', ToyBoxSchema); console.log((newToyBox()).toys); // []
To overwrite this default, you need to set the default value to undefined
constToyBoxSchema = newSchema({ toys: { type: [ToySchema], default: undefined } });
Note: specifying an empty array is equivalent to Mixed
. The following all create arrays of Mixed
:
constEmpty1 = newSchema({ any: [] }); constEmpty2 = newSchema({ any: Array }); constEmpty3 = newSchema({ any: [Schema.Types.Mixed] }); constEmpty4 = newSchema({ any: [{}] });
A MongooseMap
is a subclass of JavaScript's Map
class. In these docs, we'll use the terms 'map' and MongooseMap
interchangeably. In Mongoose, maps are how you create a nested document with arbitrary keys.
Note: In Mongoose Maps, keys must be strings in order to store the document in MongoDB.
const userSchema = newSchema({ // `socialMediaHandles` is a map whose values are strings. A map's// keys are always strings. You specify the type of values using `of`.socialMediaHandles: { type: Map, of: String } }); constUser = mongoose.model('User', userSchema); // Map { 'github' => 'vkarpov15', 'twitter' => '@code_barbarian' }console.log(newUser({ socialMediaHandles: { github: 'vkarpov15', twitter: '@code_barbarian' } }).socialMediaHandles);
The above example doesn't explicitly declare github
or twitter
as paths, but, since socialMediaHandles
is a map, you can store arbitrary key/value pairs. However, since socialMediaHandles
is a map, you must use .get()
to get the value of a key and .set()
to set the value of a key.
const user = newUser({ socialMediaHandles: {} }); // Good user.socialMediaHandles.set('github', 'vkarpov15'); // Works too user.set('socialMediaHandles.twitter', '@code_barbarian'); // Bad, the `myspace` property will **not** get saved user.socialMediaHandles.myspace = 'fail'; // 'vkarpov15'console.log(user.socialMediaHandles.get('github')); // '@code_barbarian'console.log(user.get('socialMediaHandles.twitter')); // undefined user.socialMediaHandles.github; // Will only save the 'github' and 'twitter' properties user.save();
Map types are stored as BSON objects in MongoDB. Keys in a BSON object are ordered, so this means the insertion order property of maps is maintained.
Mongoose supports a special $*
syntax to populate all elements in a map. For example, suppose your socialMediaHandles
map contains a ref
:
const userSchema = newSchema({ socialMediaHandles: { type: Map, of: newSchema({ handle: String, oauth: { type: ObjectId, ref: 'OAuth' } }) } }); constUser = mongoose.model('User', userSchema);
To populate every socialMediaHandles
entry's oauth
property, you should populate on socialMediaHandles.$*.oauth
:
const user = awaitUser.findOne().populate('socialMediaHandles.$*.oauth');
Mongoose also supports a UUID type that stores UUID instances as Node.js buffers. We recommend using ObjectIds rather than UUIDs for unique document ids in Mongoose, but you may use UUIDs if you need to.
In Node.js, a UUID is represented as an instance of bson.Binary
type with a getter that converts the binary to a string when you access it. Mongoose stores UUIDs as binary data with subtype 4 in MongoDB.
const authorSchema = newSchema({ _id: Schema.Types.UUID, // Can also do `_id: 'UUID'`name: String }); constAuthor = mongoose.model('Author', authorSchema); const bookSchema = newSchema({ authorId: { type: Schema.Types.UUID, ref: 'Author' } }); constBook = mongoose.model('Book', bookSchema); const author = newAuthor({ name: 'Martin Fowler' }); console.log(typeof author._id); // 'string'console.log(author.toObject()._idinstanceof mongoose.mongo.BSON.Binary); // trueconst book = newBook({ authorId: '09190f70-3d30-11e5-8814-0f4df9a59c41' });
To create UUIDs, we recommend using Node's built-in UUIDv4 generator.
const { randomUUID } = require('crypto'); const schema = new mongoose.Schema({ docId: { type: 'UUID', default: () =>randomUUID() } });
Mongoose supports JavaScript BigInts as a SchemaType. BigInts are stored as 64-bit integers in MongoDB (BSON type "long").
const questionSchema = newSchema({ answer: BigInt }); constQuestion = mongoose.model('Question', questionSchema); const question = newQuestion({ answer: 42n }); typeof question.answer; // 'bigint'
Mongoose supports 64-bit IEEE 754-2008 floating point numbers as a SchemaType. Doubles are stored as BSON type "double" in MongoDB.
const temperatureSchema = newSchema({ celsius: Double }); constTemperature = mongoose.model('Temperature', temperatureSchema); const temperature = newTemperature({ celsius: 1339 }); temperature.celsiusinstanceof bson.Double; // true
There are several types of values that will be successfully cast to a Double.
newTemperature({ celsius: '1.2e12' }).celsius; // 15 as a DoublenewTemperature({ celsius: true }).celsius; // 1 as a DoublenewTemperature({ celsius: false }).celsius; // 0 as a DoublenewTemperature({ celsius: { valueOf: () =>83.0033 } }).celsius; // 83 as a DoublenewTemperature({ celsius: '' }).celsius; // null
The following inputs will result will all result in a CastError once validated, meaning that it will not throw on initialization, only when validated:
valueOf()
functionMongoose supports 32-bit integers as a SchemaType. Int32s are stored as 32-bit integers in MongoDB (BSON type "int").
const studentSchema = newSchema({ id: Int32 }); constStudent = mongoose.model('Student', studentSchema); const student = newStudent({ id: 1339 }); typeof student.id; // 'number'
There are several types of values that will be successfully cast to a Number.
newStudent({ id: '15' }).id; // 15 as a Int32newStudent({ id: true }).id; // 1 as a Int32newStudent({ id: false }).id; // 0 as a Int32newStudent({ id: { valueOf: () =>83 } }).id; // 83 as a Int32newStudent({ id: '' }).id; // null as a Int32
If you pass an object with a valueOf()
function that returns a Number, Mongoose will call it and assign the returned value to the path.
The values null
and undefined
are not cast.
The following inputs will result will all result in a CastError once validated, meaning that it will not throw on initialization, only when validated:
valueOf()
functionGetters are like virtuals for paths defined in your schema. For example, let's say you wanted to store user profile pictures as relative paths and then add the hostname in your application. Below is how you would structure your userSchema
:
const root = 'https://s3.amazonaws.com/mybucket'; const userSchema = newSchema({ name: String, picture: { type: String, get: v =>`${root}${v}` } }); constUser = mongoose.model('User', userSchema); const doc = newUser({ name: 'Val', picture: '/123.png' }); doc.picture; // 'https://s3.amazonaws.com/mybucket/123.png' doc.toObject({ getters: false }).picture; // '/123.png'
Generally, you only use getters on primitive paths as opposed to arrays or subdocuments. Because getters override what accessing a Mongoose path returns, declaring a getter on an object may remove Mongoose change tracking for that path.
const schema = newSchema({ arr: [{ url: String }] }); const root = 'https://s3.amazonaws.com/mybucket'; // Bad, don't do this! schema.path('arr').get(v => { return v.map(el =>Object.assign(el, { url: root + el.url })); }); // Later doc.arr.push({ key: String }); doc.arr[0]; // 'undefined' because every `doc.arr` creates a new array!
Instead of declaring a getter on the array as shown above, you should declare a getter on the url
string as shown below. If you need to declare a getter on a nested document or array, be very careful!
const schema = newSchema({ arr: [{ url: String }] }); const root = 'https://s3.amazonaws.com/mybucket'; // Good, do this instead of declaring a getter on `arr` schema.path('arr.0.url').get(v =>`${root}${v}`);
To declare a path as another schema, set type
to the sub-schema's instance.
To set a default value based on the sub-schema's shape, simply set a default value, and the value will be cast based on the sub-schema's definition before being set during document creation.
const subSchema = new mongoose.Schema({ // some schema definition here }); const schema = new mongoose.Schema({ data: { type: subSchema, default: {} } });
Mongoose can also be extended with custom SchemaTypes. Search the plugins site for compatible types like mongoose-long, mongoose-int32, and mongoose-function.
Read more about creating custom SchemaTypes here.
schema.path()
Function The schema.path()
function returns the instantiated schema type for a given path.
const sampleSchema = newSchema({ name: { type: String, required: true } }); console.log(sampleSchema.path('name')); // Output looks like:/** * SchemaString { * enumValues: [], * regExp: null, * path: 'name', * instance: 'String', * validators: ... */
You can use this function to inspect the schema type for a given path, including what validators it has and what the type is.
Now that we've covered SchemaTypes
, let's take a look at Connections.