Since MongoDB is a schema-less NoSQL database, we use Mongoose to define a schema for our Node.js application. Mongoose is an Object Data Modeling (ODM) tool designed to work in an asynchronous environment.
You can think of a Mongoose schema as a blueprint for defining the structure of a Mongoose model that maps directly to a MongoDB collection.
A schema type is then a configuration object for an individual property within a schema. It defines the type of data a path should have, how to validate that path, the default value, whether it has any getters/setters, and other configuration options.
Here is an example:
const mongoose = require('mongoose');
const { Schema } = mongoose;
const schema = new Schema({
name: String,
joinDate: {
type: Date,
default: Date.now
}
});
schema.path('name') instanceof mongoose.SchemaType; // true
schema.path('name') instanceof mongoose.SchemaType.String; // true
The type
Attribute
As you can see above, there are two ways to define a schema type for a property. You can either use the shorthand notation (leave out type
altogether) or an object notation. If you use the object notation {...}
, you must define the type
attribute.
The type
attribute is a special property in Mongoose schemas. When Mongoose finds the type
attribute in a nested object, it automatically understands that it needs to define a property with the given schema type:
const schema = new Schema({
title: String,
author: {
type: String,
required: true
},
createdAt: {
type: Date,
default: Date.now
}
});
If the type
attribute is missing in a nested object, Mongoose treats it as a nested path as shown below:
const schema = new Schema({
name: String,
address: {
city: String,
state: String,
country: String
}
});
What if you want to define a property named type
in your Mongoose schema? It can be a little tricky.
For example, let us say that you are building a job board and you want to store the type
of the job (full-time, part-time, contract, etc.) along with other job information. Naively, you might end up defining your schema like the following:
const schema = new Schema({
title: String,
url: String,
salary: Number,
meta: {
type: String,
featured: Boolean,
highlight: Boolean
}
});
schema.path('meta').instance; // String (Wrong)
In the above example schema, you might expect meta
to be an object with three nested properties. But, unfortunately, type
is a special property in Mongoose schemas. It will assume that you want meta
to act like a string and not an object with a property type
.
For such scenarios, you have to explicitly define the type
of the type property as shown below:
const schema = new Schema({
title: String,
url: String,
salary: Number,
meta: {
type: {
type: String
},
featured: Boolean,
highlight: Boolean
}
});
Available Schema Types
Mongoose has the following schema types that you can use to define a schema:
String
The string is one of the most popular data types in computer programming along with numbers. To define as a string, you may either use the String
global constructor or the string 'String'
as shown below:
const schema = new Schema({
title: String,
url: 'String'
});
const Post = mongoose.model('Post', schema);
When you assign a value to a string property, Mongoose automatically attempts to convert it into a string. If you pass a value that has the toString()
function, Mongoose will call it unless the property is an array:
new Post({ title: 43 }).title; // "43"
new Post({ title: { toString: () => 43 } }); // "43"
new Post({ title: { job: 43 } }); // Error on `save()`
Number
To define a path as a number, you can either use the Number
global constructor or the string 'Number'
:
const schema = new Schema({
age: Number,
salary: 'Number'
});
const User = mongoose.model('User', schema);
Mongoose automatically casts several types of values to a number:
new User({ age: '23' }).age; // 23
new User({ age: true }).age; // 1
If you pass an object with a valueOf()
method that evaluates to a number, Mongoose will call it and assign the returned value to the property:
new User({ salary: valueOf = () => 1500 }).salary; // 1500
The values null
and undefined
are not cast to a number:
new User({ age: null }).age; // null
new User({ age: undefined }).age; // undefined
NaN
, strings that cast to NaN
, arrays, and objects that do not have a valueOf()
function are also not cast to a number and results in a CastError
error upon validation.
Date
To declare a path as a date, you can use the global Date
object:
const schema = new Schema({
name: String,
age: Number,
dateOfBirth: Date
});
const User = mongoose.model('User', schema);
Buffer
To declare a path as a buffer, you can use the global Buffer
constructor or the string Buffer
:
const schema = new Schema({
meta: Buffer,
data: 'Buffer'
});
const Server = mongoose.model('Server', schema);
Mongoose will automatically cast the below values to buffers in Node.js:
const s1 = new Server({ meta: 'data' });
const s2 = new Server({ meta: 345 });
const s3 = new Server({ meta: { type: 'Buffer', data: [4, 5, 6] } });
Mixed
The mixed schema type means "anything goes". The property that has a mixed schema type can hold any value. Mongoose will not do any datacasting on mixed properties.
There are several ways to define a mixed path. The following are equivalent:
const schema = new Schema({ meta: {} });
const schema = new Schema({ meta: Object });
const schema = new Schema({ meta: Schema.Types.Mixed });
const schema = new Schema({ meta: mongoose.Mixed });
ObjectId
The ObjectId
is a special schema type in Mongoose that is used for storing unique values. To declare a property as a unique identifier, you can use the ObjectId
type as shown below:
const schema = new Schema({
identifier: mongoose.ObjectId,
name: String
});
const Car = mongoose.model('Car', schema);
To assign an ObjectId
to a property, you can use the mongoose.Types.ObjectId
constructor:
const car = new Car({
identifier: new mongoose.Types.ObjectId(),
name: 'BMW'
});
typeof car.identifier; // object
car.identifier instanceof mongoose.Types.ObjectId; // true
car.identifier; // 5febbcbb57a9528bcdacc4bd
Boolean
In Mongoose, booleans are just plain JavaScript booleans. By default, Mongoose casts the following values to true
:
true
'true'
1
'1'
'yes'
The following values are cast to false
by Mongoose:
false
'false'
0
'0'
'no'
const schema = new Schema({
name: String,
admin: Boolean,
age: Number
});
Arrays
The array is the most commonly used data structure in JavaScript and other programming languages for storing multiple data chunks in a single variable.
Mongoose supports both arrays of other schema types as well as arrays of sub-documents. Arrays of schema types are also known as primitive arrays, and arrays of sub-documents are also called document arrays:
const comments = new Schema({ body: String });
const posts = new Schema({
title: String,
upvotes: [Number],
favorites: [String],
comments: [comments]
});
const Post = mongoose.model('Post', posts);
By default, any property with the array schema type has a default value of []
(empty array):
const post = new Post();
console.log(post.upvotes); // []
console.log(post.comments); // []
To overwrite this default value, you need to provide a default value for the property:
const schema = new Schema({
upvotes: {
type: [Number],
default: undefined
}
});
Maps
Maps in Mongoose is a sub-class of the JavaScript's map class. They are used by Mongoose to create nested documents with arbitrary keys:
const schema = new Schema({
name: String,
attributes: {
type: Map,
of: String
}
});
const User = mongoose.model('User', schema);
Notice the attributes
property above. It has a type of Map
along with one additional key of
. The of
key defines the type of values will this map holds.
In Mongoose maps, keys must be strings to store the document in MongoDB:
const user = new User({
name: 'Alex',
attributes: {
job: 'Software Engineer',
age: '25'
}
});
// {
// _id: 5fee2e799e6627a3f3334343,
// name: 'Alex',
// attributes: Map(2) { 'job' => 'Software Engineer', 'age' => '25' }
// }
To get and set the value of a key, you must use get()
and set()
methods of the Map
class respectively:
user.attributes.get('job'); // Software Engineer
user.attributes.get('age'); // 25
user.attributes.set('age', '31');
user.attributes.get('age'); // 31
Schema Type Options
In addition to the type
property, you can also define additional properties for a path. For example, if you want to change the case of the string, you can use lowercase
and uppercase
properties:
const schema = new Schema({
title: String,
permalink: {
type: String,
lowercase: true
},
uuid: {
type: String,
uppercase: true
}
});
Similarly, if you want to make sure a path always exists, use the required
property to add the required validator:
const schema = new Schema({
name: {
type: String,
required: true
}
});
✌️ Like this article? Follow me on Twitter and LinkedIn. You can also subscribe to RSS Feed.