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 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 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 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 instead of 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
Strings and numbers are the most popular data types in computer programming. 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 result 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 used by Mongoose 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 the get()
and set()
methods of the Map
class:
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 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.