Aside from code reuse, one important reason to use subdocuments is to create a path where there would otherwise not be one to allow for validation over a group of fields (e.g. dateRange.fromDate <= dateRange.toDate).

    Subdocuments are similar to normal documents. Nested schemas can have middleware, , virtuals, and any other feature top-level schemas can use. The major difference is that subdocuments are not saved individually, they are saved whenever their top-level parent document is saved.

    1. const parent = new Parent({ children: [{ name: 'Matt' }, { name: 'Sarah' }] })
    2. parent.children[0].name = 'Matthew';
    3. // `parent.children[0].save()` is a no-op, it triggers middleware but
    4. // does **not** actually save the subdocument. You need to save the parent
    5. // doc.
    6. parent.save(callback);

    Subdocuments have save and validate middleware just like top-level documents. Calling save() on the parent document triggers the save() middleware for all its subdocuments, and the same for validate() middleware.

    1. childSchema.pre('save', function (next) {
    2. if ('invalid' == this.name) {
    3. return next(new Error('#sadpanda'));
    4. }
    5. next();
    6. });
    7. const parent = new Parent({ children: [{ name: 'invalid' }] });
    8. parent.save(function (err) {
    9. console.log(err.message) // #sadpanda
    10. });

    Subdocuments’ pre('save') and pre('validate') middleware execute before the top-level document’s pre('save') but after the top-level document’s pre('validate') middleware. This is because validating before save() is actually a piece of built-in middleware.

    1. // Below code will print out 1-4 in order
    2. const childSchema = new mongoose.Schema({ name: 'string' });
    3. childSchema.pre('validate', function(next) {
    4. console.log('2');
    5. next();
    6. });
    7. childSchema.pre('save', function(next) {
    8. console.log('3');
    9. next();
    10. });
    11. const parentSchema = new mongoose.Schema({
    12. child: childSchema
    13. });
    14. parentSchema.pre('validate', function(next) {
    15. console.log('1');
    16. next();
    17. });
    18. parentSchema.pre('save', function(next) {
    19. console.log('4');
    20. });

    Subdocuments versus Nested Paths

    In Mongoose, nested paths are subtly different from subdocuments. For example, below are two schemas: one with child as a subdocument, and one with child as a nested path.

    1. // Subdocument
    2. const subdocumentSchema = new mongoose.Schema({
    3. child: new mongoose.Schema({ name: String, age: Number })
    4. });
    5. const Subdoc = mongoose.model('Subdoc', subdocumentSchema);
    6. // Nested path
    7. const nestedSchema = new mongoose.Schema({
    8. child: { name: String, age: Number }
    9. });
    10. const Nested = mongoose.model('Nested', nestedSchema);

    These two schemas look similar, and the documents in MongoDB will have the same structure with both schemas. But there are a few Mongoose-specific differences:

    1. const doc1 = new Subdoc({});
    2. doc1.child === undefined; // true
    3. doc1.child.name = 'test'; // Throws TypeError: cannot read property...
    4. const doc2 = new Nested({});
    5. doc2.child === undefined; // false
    6. console.log(doc2.child); // Prints 'MongooseDocument { undefined }'
    7. doc2.child.name = 'test'; // Works

    Secondly, in Mongoose 5, Document#set() merges when you call it on a nested path, but overwrites when you call it on a subdocument.

    Subdocument paths are undefined by default, and Mongoose does not apply subdocument defaults unless you set the subdocument path to a non-nullish value.

    1. const subdocumentSchema = new mongoose.Schema({
    2. child: new mongoose.Schema({
    3. name: String,
    4. age: {
    5. type: Number,
    6. default: 0
    7. }
    8. })
    9. });
    10. const Subdoc = mongoose.model('Subdoc', subdocumentSchema);
    11. // Note that the `age` default has no effect, because `child`
    12. // is `undefined`.
    13. const doc = new Subdoc();
    14. doc.child; // undefined

    However, if you set doc.child to any object, Mongoose will apply the age default if necessary.

    1. doc.child = {};
    2. // Mongoose applies the `age` default:
    3. doc.child.age; // 0

    Mongoose applies defaults recursively, which means there’s a nice workaround if you want to make sure Mongoose applies subdocument defaults: make the subdocument path default to an empty object.

    1. const childSchema = new mongoose.Schema({
    2. name: String,
    3. age: {
    4. type: Number,
    5. default: 0
    6. }
    7. });
    8. const subdocumentSchema = new mongoose.Schema({
    9. child: {
    10. type: childSchema,
    11. default: () => ({})
    12. }
    13. });
    14. const Subdoc = mongoose.model('Subdoc', subdocumentSchema);
    15. // Note that Mongoose sets `age` to its default value 0, because
    16. // `child` defaults to an empty object and Mongoose applies
    17. // defaults to that empty object.
    18. const doc = new Subdoc();
    19. doc.child; // { age: 0 }

    Finding a Subdocument

    Each subdocument has an _id by default. Mongoose document arrays have a special id method for searching a document array to find a document with a given _id.

    1. const doc = parent.children.id(_id);

    MongooseArray methods such as , unshift, , and others cast arguments to their proper types transparently:

    1. const Parent = mongoose.model('Parent');
    2. const parent = new Parent;
    3. parent.children.push({ name: 'Liesl' });
    4. const subdoc = parent.children[0];
    5. console.log(subdoc) // { _id: '501d86090d371bab2c0341c5', name: 'Liesl' }
    6. subdoc.isNew; // true
    7. parent.save(function (err) {
    8. if (err) return handleError(err)
    9. console.log('Success!');

    Removing Subdocs

    Each subdocument has it’s own method. For an array subdocument, this is equivalent to calling .pull() on the subdocument. For a single nested subdocument, remove() is equivalent to setting the subdocument to null.

    1. // Equivalent to `parent.children.pull(_id)`
    2. parent.children.id(_id).remove();
    3. // Equivalent to `parent.child = null`
    4. parent.child.remove();
    5. parent.save(function (err) {
    6. if (err) return handleError(err);
    7. console.log('the subdocs were removed');
    8. });

    Sometimes, you need to get the parent of a subdoc. You can access the parent using the parent() function.

    1. const schema = new Schema({
    2. docArr: [{ name: String }],
    3. singleNested: new Schema({ name: String })
    4. });
    5. const Model = mongoose.model('Test', schema);
    6. const doc = new Model({
    7. docArr: [{ name: 'foo' }],
    8. singleNested: { name: 'bar' }
    9. });
    10. doc.singleNested.parent() === doc; // true
    11. doc.docArr[0].parent() === doc; // true

    If you have a deeply nested subdoc, you can access the top-level document using the ownerDocument() function.

    1. const schema = new Schema({
    2. level1: new Schema({
    3. level2: new Schema({
    4. test: String
    5. })
    6. })
    7. });
    8. const Model = mongoose.model('Test', schema);
    9. const doc = new Model({ level1: { level2: 'test' } });
    10. doc.level1.level2.parent() === doc; // false
    11. doc.level1.level2.parent() === doc.level1; // true
    12. doc.level1.level2.ownerDocument() === doc; // true

    Alternate declaration syntax for arrays

    If you create a schema with an array of objects, Mongoose will automatically convert the object to a schema for you:

    1. const parentSchema = new Schema({
    2. children: [{ name: 'string' }]
    3. });
    4. // Equivalent
    5. const parentSchema = new Schema({
    6. children: [new Schema({ name: 'string' })]
    7. });

    Alternate declaration syntax for single nested subdocuments

    Unlike document arrays, Mongoose 5 does not convert an objects in schemas into nested schemas. In the below example, nested is a nested path rather than a subdocument.

    1. const schema = new Schema({
    2. nested: {
    3. prop: String
    4. }
    5. });

    This leads to some surprising behavior when you attempt to define a nested path with validators or getters/setters.

    1. const schema = new Schema({
    2. nested: {
    3. // Because of `typePojoToMixed`, Mongoose knows to
    4. // wrap `{ prop: String }` in a `new Schema()`.
    5. type: { prop: String },
    6. required: true
    7. }, { typePojoToMixed: false });

    Next Up

    Now that we’ve covered Subdocuments, let’s take a look at .