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:

  1. true
  2. 'true'
  3. 1
  4. '1'
  5. 'yes'

The following values are cast to false by Mongoose:

  1. false
  2. 'false'
  3. 0
  4. '0'
  5. '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.