# Introduction to Mongoose schema types

By Atta Ur Rehman Shah (https://attacomsian.com/about). Published Dec 31, 2020, updated Oct 3, 2022. Topics: Node.js, Mongoose, MongoDB.
Canonical URL: https://attacomsian.com/blog/mongoose-schema-types

> An introduction to Mongoose's available schema types and how you can use them to define a schema for your model in a Node.js application.

Since MongoDB is a schema-less NoSQL database, we use [Mongoose](https://attacomsian.com/blog/nodejs-getting-started-with-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](https://attacomsian.com/blog/mongoose-schemas) as a blueprint for defining the structure of a Mongoose [model](https://attacomsian.com/blog/mongoose-models) 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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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'`:

```javascript
const schema = new Schema({
  age: Number,
  salary: 'Number'
})

const User = mongoose.model('User', schema)
```

Mongoose automatically casts several types of values to a number:

```javascript
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:

```javascript
new User({ salary: valueOf = () => 1500 }).salary    // 1500
```

The values `null` and `undefined` are not cast to a number:

```javascript
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:

```javascript
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`:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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'`

```javascript
const schema = new Schema({
  name: String,
  admin: Boolean,
  age: Number
})
```

### Arrays

The [array](https://attacomsian.com/blog/javascript-arrays) 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**:

```javascript
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):

```javascript
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:

```javascript
const schema = new Schema({
  upvotes: {
    type: [Number],
    default: undefined
  }
})
```

### Maps

Maps in Mongoose is a sub-class of the [JavaScript's map class](https://attacomsian.com/blog/maps-javascript). They are used by Mongoose to create nested documents with arbitrary keys:

```javascript
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](https://attacomsian.com/blog/mongoose-documents) in MongoDB:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
const schema = new Schema({
  name: {
    type: String,
    required: true
  }
})
```
