Mongoose: Defining a schema with nested objects

Updated: December 30, 2023 By: Guest Contributor Post a comment

Introduction

Mongoose is an Object Document Mapper (ODM) library for MongoDB and Node.js. It manages relationships between data, provides schema validation, and is used to translate between objects in code and the representation of those objects in MongoDB. In this tutorial, you’ll learn how to define a Mongoose schema with nested objects, offering you the ability to create more organized and structured data models reflecting real-world entities with multiple hierarchical layers.

Throughout the guide, we’ll increase complexity gradually, starting from a simple nested object scenario and gradually moving to more complex data structures. By the end, you’ll have a comprehensive understanding of handling nested schemas in Mongoose.

Defining a Simple Nested Schema

Firstly, let’s look at how we might define a basic nested schema in Mongoose. Assume we’re managing a collection of ‘Books’ each belongs to a specific ‘author’ who has a ‘name’ and an ’email’:

const mongoose = require('mongoose');
const { Schema } = mongoose;

const authorSchema = new Schema({
  name: String,
  email: String
});

const bookSchema = new Schema({
  title: String,
  publishedYear: Number,
  author: authorSchema
});

const Book = mongoose.model('Book', bookSchema);

In the code above, we have clearly defined a simple nested schema within our ‘bookSchema’. In a ‘Book’, the ‘author’ field is not a simple property; instead, it is now structured according to the ‘authorSchema’.

Using References: Populating Nested Schemas

Sometimes you’ll want to reference another collection instead of nesting the entire object. This is how relationships are maintained in MongoDB using ObjectId references and the populate feature of Mongoose:

const authorSchema = new Schema({
  _id: Schema.Types.ObjectId,
  name: String,
  email: String
});

const bookSchema = new Schema({
  title: String,
  publishedYear: Number,
  author: { type: Schema.Types.ObjectId, ref: 'Author' }
});

const Author = mongoose.model('Author', authorSchema);
const Book = mongoose.model('Book', bookSchema);

This setup allows us to store a reference to ‘author’ by their ObjectId. And with the use of Mongoose’s ‘populate()’ method, you can fetch the author’s details whenever you query for books. However, for the sake of this tutorial, we’ll focus on defining and using nested objects directly embedded in the parent schema.

Adding Arrays of Nested Objects

Going further, collections might require arrays of nested objects. For instance, if a book can have multiple authors, we can model this as follows:

const bookSchema = new Schema({
  title: String,
  publishedYear: Number,
  authors: [authorSchema]
});

const Book = mongoose.model('Book', bookSchema);

Here, ‘authors’ is an array that can hold multiple ‘authorSchema’ instances. Given that flexibility, each book document can accommodate several authors.

Advanced Nesting and Sub-Documents

In more complex scenarios, you may have multi-level nesting which translates to sub-documents in Mongoose. For example, authors might have addresses with several fields:

const addressSchema = new Schema({
  street: String,
  city: String,
  country: String
});

const authorSchema = new Schema({
  name: String,
  email: String,
  address: addressSchema
});

const bookSchema = new Schema({
  title: String,
  publishedYear: Number,
  authors: [authorSchema]
});

// The rest remains unchanged

Here we have a schema for addresses embedded within our ‘authorSchema’ which in turn is embedded into the ‘bookSchema’. This is a practical approach to sculpting complex, structured data models using Mongoose.

Validating Nested Object Data

Mongoose’s schema system comes equipped with validation rules. You can apply these to nested objects just as you would to top-level schema fields. Consider extending the ‘addressSchema’ to include some basic validations:

const addressSchema = new Schema({
  street: { type: String, required: true },
  city: { type: String, required: true },
  country: { type: String, required: [true, 'Country is required.'] }
});

// The rest of the bookSchema and authorSchema follows these definitions as shown above

By adding ‘required’ validators, Mongoose will ensure the completeness of address data when attempting to save a book document.

Conclusion

In conclusion, defining a schema with nested objects in Mongoose is an excellent way to reflect complex hierarchical data structures. Especially with real-world applications where relationships need concise, organized molds, using nested objects simply make sense. This guide has covered the definitions from simple to advanced levels, validation rules for sub-documents, and more. With the knowledge you’ve gained here, structuring documents in your MongoDB collections will become much more intuitive and maintainable.