Adding Validation for Embedded Array in Array Objects in Mongoose

written in guide, javascript, mongoose

Surprisingly there was very little information I could find when googling this issue. Granted, it’s a very niche issue but here’s what I personally found when trying things out. If you’re unfamiliar with mongoose’s validation please consult the guide first. This document assumes some familiarity with mongoose schemas, embedded schemas, and validation.

There’s a lot of pre-explanation. if you want to go directly to the answer head to the end of the page

So in mongoose, there’s several ways to define a schema. Once a Schema is defined, you can require mongoose to put custom validation on the object before it gets saved into the database. Mongoose documentation recommends you use the Schema#path(path, constructor) function to get the SchemaType object where you can then use SchemaType#validate(obj, [errorMsg]) on. The problem occurs when your path branches into an array.

The following examples are attempting to model a Car object that can have one or more features.

creating a schema with an array of an embedded object
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var CarSchema = new Schema({
    name: {
        type: String,
        required: true,
    },
    features: [{
        name: {
            type: String,
            required: true //this 'required' option only implies that if creating an object in the features array, the object better have a 'name' attribute in it.  Does not mean that the 'features' array is required.
        }
    }]
});

mongoose.model('Car', CarSchema);

the following below creates two schemas, car and features, and puts a reference of features in cars similar to how typical RDBMS work.

creating a schema with an array by creating multiple schemas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var CarSchema = new Schema({
    name: {
        type: String,
        required: true,
    },
    features: [{
        type: Schema.ObjectId,
        ref: Feature
        required: true; //will make it so that the feature's array will be required to save this document
    }]
});

var FeatureSchema = new Schema({
    name: {
        type: String,
        required: true
    }
});

mongoose.model('Car', CarSchema);
mongoose.model('Feature', FeatureSchema);

Each method of creating mongoose objects has some benefits and tradeoffs.

Embedded Object in Single Schema

Pros
  • data access is simpler, requires no joining of documents
  • less boilerplate code to write
Cons
  • If data is large, can get messy
  • Not possible to set SchemaType options directly in Schema

Objects split between multiple Schemas

Pros
  • Similar to RDBMS normalization structure
  • data is more scalable and reusable
  • Mongoose supports the addition of validation through SchemaType options in the Schema declaration itself
Cons
  • Increased amount of boilerplate code needed for Schema creation.
  • filesystem can get messy with large amount of tables required for a simple object.

Lets talk about adding validation

Lets say that we’re trying to add a ‘required’ validation. This validation will check to see if the Car object has a features array with a feature in it. If it doesn’t, the save will fail, if it does, then the document will be stored in the database.

Adding validation to embedded Schema objects
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var CarSchema = new Schema({
    name: {
        type: String,
        required: true,
    },
    features: [{
        name: {
            type: String,
            required: true
        }
    }]

});

CarSchema.path('features').validate(function(features){
    if(!features){return false}
    else if(features.length === 0){return false}
    return true;
}, 'Car needs to have at least one feature');

mongoose.model('Car', CarSchema);

When embedded objects are used to define a schema, you first have to use Schema#path(path, constructor) to get the Schema object and then use its validate function.

As for the second example, the validation is already set with the ‘required’ option being set on the ‘features’ attribute. By not declaring the Schema internally, you can place options such as ‘required’ and ‘validate’ in the Schema declaration. Refer to this page to see what other SchemaType options there are.

Adding validation module Schema declarations
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var mongoose = require('mongoose');
var Schema = mongoose.Schema;

//the custom validation that will get applied to the features attribute.
var notEmpty = function(features){
    if(features.length === 0){return false}
    else {return true};
}

var CarSchema = new Schema({
    name: {
        type: String,
        required: true,
    },
    features: [{
        type: Schema.ObjectId,
        ref: Feature
        required: true; //this will prevent a Car model from being saved without a features array.
        validate: [notEmpty, 'Please add at least one feature in the features array'] //this adds custom validation through the function check of notEmpty.
    }]
});

var FeatureSchema = new Schema({
    name: {
        type: String,
        required: true //this will prevent a Feature model from being saved without a name attribute.
    }
});

mongoose.model('Car', CarSchema);
mongoose.model('Feature', FeatureSchema);

Adding validation to a Schema with an array in an array

What if you had an embedded Schema that declared an embedded object in an array that declared another embedded object inside of another array. This is the tricky part that’s not very well documented.

Adding validation to embedded Schema objects
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var CarSchema = new Schema({
    name: {
        type: String,
        required: true,
    },
    features: [{
        name: {
            type: String,
            required: true
        },
        model: [{
            year: {
                type: Number
            }
        }]
    }]

});

CarSchema.path('features').schema.path('model').schema.path('year').validate(function(features){
    if(!features){return false}
    else if(features.length === 0){return false}
    return true;
}, 'Car needs to have at least one feature');

mongoose.model('Car', CarSchema);

You’re unable to specify the path when the object is behind arrays. Accessing the Schema in the array itself cannot be accessed like CarSchema.path(‘features.model.year’), but it can be accessed by grabbing the schema attribute which returns the SchemaType of the object in the array. Now that we have the SchemaType object we apply its path to continue the chain until we get the attribute that we want to apply the validation for. As far as I know, the DocumentArray.schema attribute is not a documented feature.


Comments