Before we get into the specifics of validation syntax, please keep the following rules in mind:
pre('save')
hook on every schema by default.pre('save')
hook. This means that validation doesn't run on any changes you make in pre('save')
hooks.doc.validate()
or doc.validateSync()
doc.invalidate(...)
required
validator.const schema = newSchema({ name: { type: String, required: true } }); constCat = db.model('Cat', schema); // This cat has no name :(const cat = newCat(); let error; try { await cat.save(); } catch (err) { error = err; } assert.equal(error.errors['name'].message, 'Path `name` is required.'); error = cat.validateSync(); assert.equal(error.errors['name'].message, 'Path `name` is required.');
unique
Option is Not a Validatorthis
Mongoose has several built-in validators.
checkRequired()
function to determine if the value satisfies the required validator.min
and max
validators.enum
, match
, minLength
, and maxLength
validators.Each of the validator links above provide more information about how to enable them and customize their error messages.
const breakfastSchema = newSchema({ eggs: { type: Number, min: [6, 'Too few eggs'], max: 12 }, bacon: { type: Number, required: [true, 'Why no bacon?'] }, drink: { type: String, enum: ['Coffee', 'Tea'], required: function() { returnthis.bacon > 3; } } }); constBreakfast = db.model('Breakfast', breakfastSchema); const badBreakfast = newBreakfast({ eggs: 2, bacon: 0, drink: 'Milk' }); let error = badBreakfast.validateSync(); assert.equal(error.errors['eggs'].message, 'Too few eggs'); assert.ok(!error.errors['bacon']); assert.equal(error.errors['drink'].message, '`Milk` is not a valid enum value for path `drink`.'); badBreakfast.bacon = 5; badBreakfast.drink = null; error = badBreakfast.validateSync(); assert.equal(error.errors['drink'].message, 'Path `drink` is required.'); badBreakfast.bacon = null; error = badBreakfast.validateSync(); assert.equal(error.errors['bacon'].message, 'Why no bacon?');
You can configure the error message for individual validators in your schema. There are two equivalent ways to set the validator error message:
min: [6, 'Must be at least 6, got {VALUE}']
enum: { values: ['Coffee', 'Tea'], message: '{VALUE} is not supported' }
Mongoose also supports rudimentary templating for error messages. Mongoose replaces {VALUE}
with the value being validated.
const breakfastSchema = newSchema({ eggs: { type: Number, min: [6, 'Must be at least 6, got {VALUE}'], max: 12 }, drink: { type: String, enum: { values: ['Coffee', 'Tea'], message: '{VALUE} is not supported' } } }); constBreakfast = db.model('Breakfast', breakfastSchema); const badBreakfast = newBreakfast({ eggs: 2, drink: 'Milk' }); const error = badBreakfast.validateSync(); assert.equal(error.errors['eggs'].message, 'Must be at least 6, got 2'); assert.equal(error.errors['drink'].message, 'Milk is not supported');
unique
Option is Not a Validator A common gotcha for beginners is that the unique
option for schemas is not a validator. It's a convenient helper for building MongoDB unique indexes. See the FAQ for more information.
const uniqueUsernameSchema = newSchema({ username: { type: String, unique: true } }); constU1 = db.model('U1', uniqueUsernameSchema); constU2 = db.model('U2', uniqueUsernameSchema); const dup = [{ username: 'Val' }, { username: 'Val' }]; // Race condition! This may save successfully, depending on whether// MongoDB built the index before writing the 2 docs.U1.create(dup). then(() => { }). catch(err => { }); // You need to wait for Mongoose to finish building the `unique`// index before writing. You only need to build indexes once for// a given collection, so you normally don't need to do this// in production. But, if you drop the database between tests,// you will need to use `init()` to wait for the index build to finish.U2.init(). then(() =>U2.create(dup)). catch(error => { // `U2.create()` will error, but will *not* be a mongoose validation error, it will be// a duplicate key error.// See: https://masteringjs.io/tutorials/mongoose/e11000-duplicate-key assert.ok(error); assert.ok(!error.errors); assert.ok(error.message.indexOf('duplicate key error') !== -1); });
If the built-in validators aren't enough, you can define custom validators to suit your needs.
Custom validation is declared by passing a validation function. You can find detailed instructions on how to do this in the SchemaType#validate()
API docs.
const userSchema = newSchema({ phone: { type: String, validate: { validator: function(v) { return/\d{3}-\d{3}-\d{4}/.test(v); }, message: props =>`${props.value} is not a valid phone number!` }, required: [true, 'User phone number required'] } }); constUser = db.model('user', userSchema); const user = newUser(); let error; user.phone = '555.0123'; error = user.validateSync(); assert.equal(error.errors['phone'].message, '555.0123 is not a valid phone number!'); user.phone = ''; error = user.validateSync(); assert.equal(error.errors['phone'].message, 'User phone number required'); user.phone = '201-555-0123'; // Validation succeeds! Phone number is defined// and fits `DDD-DDD-DDDD` error = user.validateSync(); assert.equal(error, null);
Custom validators can also be asynchronous. If your validator function returns a promise (like an async
function), mongoose will wait for that promise to settle. If the returned promise rejects, or fulfills with the value false
, Mongoose will consider that a validation error.
const userSchema = newSchema({ name: { type: String, // You can also make a validator async by returning a promise.validate: () =>Promise.reject(newError('Oops!')) }, email: { type: String, // There are two ways for an promise-based async validator to fail:// 1) If the promise rejects, Mongoose assumes the validator failed with the given error.// 2) If the promise resolves to `false`, Mongoose assumes the validator failed and creates an error with the given `message`.validate: { validator: () =>Promise.resolve(false), message: 'Email validation failed' } } }); constUser = db.model('User', userSchema); const user = newUser(); user.email = 'test@test.co'; user.name = 'test'; let error; try { await user.validate(); } catch (err) { error = err; } assert.ok(error); assert.equal(error.errors['name'].message, 'Oops!'); assert.equal(error.errors['email'].message, 'Email validation failed');
Errors returned after failed validation contain an errors
object whose values are ValidatorError
objects. Each ValidatorError has kind
, path
, value
, and message
properties. A ValidatorError also may have a reason
property. If an error was thrown in the validator, this property will contain the error that was thrown.
const toySchema = newSchema({ color: String, name: String }); const validator = function(value) { return/red|white|gold/i.test(value); }; toySchema.path('color').validate(validator, 'Color `{VALUE}` not valid', 'Invalid color'); toySchema.path('name').validate(function(v) { if (v !== 'Turbo Man') { thrownewError('Need to get a Turbo Man for Christmas'); } returntrue; }, 'Name `{VALUE}` is not valid'); constToy = db.model('Toy', toySchema); const toy = newToy({ color: 'Green', name: 'Power Ranger' }); let error; try { await toy.save(); } catch (err) { error = err; } // `error` is a ValidationError object// `error.errors.color` is a ValidatorError object assert.equal(error.errors.color.message, 'Color `Green` not valid'); assert.equal(error.errors.color.kind, 'Invalid color'); assert.equal(error.errors.color.path, 'color'); assert.equal(error.errors.color.value, 'Green'); // If your validator throws an exception, mongoose will use the error// message. If your validator returns `false`,// mongoose will use the 'Name `Power Ranger` is not valid' message. assert.equal(error.errors.name.message, 'Need to get a Turbo Man for Christmas'); assert.equal(error.errors.name.value, 'Power Ranger'); // If your validator threw an error, the `reason` property will contain// the original error thrown, including the original stack trace. assert.equal(error.errors.name.reason.message, 'Need to get a Turbo Man for Christmas'); assert.equal(error.name, 'ValidationError');
Before running validators, Mongoose attempts to coerce values to the correct type. This process is called casting the document. If casting fails for a given path, the error.errors
object will contain a CastError
object.
Casting runs before validation, and validation does not run if casting fails. That means your custom validators may assume v
is null
, undefined
, or an instance of the type specified in your schema.
const vehicleSchema = new mongoose.Schema({ numWheels: { type: Number, max: 18 } }); constVehicle = db.model('Vehicle', vehicleSchema); const doc = newVehicle({ numWheels: 'not a number' }); const err = doc.validateSync(); err.errors['numWheels'].name; // 'CastError'// 'Cast to Number failed for value "not a number" at path "numWheels"' err.errors['numWheels'].message;
By default, Mongoose cast error messages look like Cast to Number failed for value "pie" at path "numWheels"
. You can overwrite Mongoose's default cast error message by the cast
option on your SchemaType to a string as follows.
const vehicleSchema = new mongoose.Schema({ numWheels: { type: Number, cast: '{VALUE} is not a number' } }); constVehicle = db.model('Vehicle', vehicleSchema); const doc = newVehicle({ numWheels: 'pie' }); const err = doc.validateSync(); err.errors['numWheels'].name; // 'CastError'// "pie" is not a number err.errors['numWheels'].message;
Mongoose's cast error message templating supports the following parameters:
{PATH}
: the path that failed to cast{VALUE}
: a string representation of the value that failed to cast{KIND}
: the type that Mongoose attempted to cast to, like 'String'
or 'Number'
You can also define a function that Mongoose will call to get the cast error message as follows.
const vehicleSchema = new mongoose.Schema({ numWheels: { type: Number, cast: [null, (value, path, model, kind) =>`"${value}" is not a number`] } }); constVehicle = db.model('Vehicle', vehicleSchema); const doc = newVehicle({ numWheels: 'pie' }); const err = doc.validateSync(); err.errors['numWheels'].name; // 'CastError'// "pie" is not a number err.errors['numWheels'].message;
In addition to defining custom validators on individual schema paths, you can also configure a custom validator to run on every instance of a given SchemaType
. For example, the following code demonstrates how to make empty string ''
an invalid value for all string paths.
// Add a custom validator to all strings mongoose.Schema.Types.String.set('validate', v => v == null || v > 0); const userSchema = newSchema({ name: String, email: String }); constUser = db.model('User', userSchema); const user = newUser({ name: '', email: '' }); const err = await user.validate().then(() =>null, err => err); err.errors['name']; // ValidatorError err.errors['email']; // ValidatorError
Defining validators on nested objects in mongoose is tricky, because nested objects are not fully fledged paths.
let personSchema = newSchema({ name: { first: String, last: String } }); assert.throws(function() { // This throws an error, because 'name' isn't a full fledged path personSchema.path('name').required(true); }, /Cannot.*'required'/); // To make a nested object required, use a single nested schemaconst nameSchema = newSchema({ first: String, last: String }); personSchema = newSchema({ name: { type: nameSchema, required: true } }); constPerson = db.model('Person', personSchema); const person = newPerson(); const error = person.validateSync(); assert.ok(error.errors['name']);
In the above examples, you learned about document validation. Mongoose also supports validation for update()
, updateOne()
, updateMany()
, and findOneAndUpdate()
operations. Update validators are off by default - you need to specify the runValidators
option.
To turn on update validators, set the runValidators
option for update()
, updateOne()
, updateMany()
, or findOneAndUpdate()
. Be careful: update validators are off by default because they have several caveats.
const toySchema = newSchema({ color: String, name: String }); constToy = db.model('Toys', toySchema); Toy.schema.path('color').validate(function(value) { return/red|green|blue/i.test(value); }, 'Invalid color'); const opts = { runValidators: true }; let error; try { awaitToy.updateOne({}, { color: 'not a color' }, opts); } catch (err) { error = err; } assert.equal(error.errors.color.message, 'Invalid color');
this
There are a couple of key differences between update validators and document validators. In the color validation function below, this
refers to the document being validated when using document validation. However, when running update validators, this
refers to the query object instead of the document. Because queries have a neat .get()
function, you can get the updated value of the property you want.
const toySchema = newSchema({ color: String, name: String }); toySchema.path('color').validate(function(value) { // When running in `validate()` or `validateSync()`, the// validator can access the document using `this`.// When running with update validators, `this` is the Query,// **not** the document being updated!// Queries have a `get()` method that lets you get the// updated value.if (this.get('name') && this.get('name').toLowerCase().indexOf('red') !== -1) { return value === 'red'; } returntrue; }); constToy = db.model('ActionFigure', toySchema); const toy = newToy({ color: 'green', name: 'Red Power Ranger' }); // Validation failed: color: Validator failed for path `color` with value `green`let error = toy.validateSync(); assert.ok(error.errors['color']); const update = { color: 'green', name: 'Red Power Ranger' }; const opts = { runValidators: true }; error = null; try { awaitToy.updateOne({}, update, opts); } catch (err) { error = err; } // Validation failed: color: Validator failed for path `color` with value `green` assert.ok(error);
The other key difference is that update validators only run on the paths specified in the update. For instance, in the below example, because 'name' is not specified in the update operation, update validation will succeed.
When using update validators, required
validators only fail when you try to explicitly $unset
the key.
const kittenSchema = newSchema({ name: { type: String, required: true }, age: Number }); constKitten = db.model('Kitten', kittenSchema); const update = { color: 'blue' }; const opts = { runValidators: true }; // Operation succeeds despite the fact that 'name' is not specifiedawaitKitten.updateOne({}, update, opts); const unset = { $unset: { name: 1 } }; // Operation fails because 'name' is requiredconst err = awaitKitten.updateOne({}, unset, opts).then(() =>null, err => err); assert.ok(err); assert.ok(err.errors['name']);
One final detail worth noting: update validators only run on the following update operators:
$set
$unset
$push
$addToSet
$pull
$pullAll
For instance, the below update will succeed, regardless of the value of number
, because update validators ignore $inc
.
Also, $push
, $addToSet
, $pull
, and $pullAll
validation does not run any validation on the array itself, only individual elements of the array.
const testSchema = newSchema({ number: { type: Number, max: 0 }, arr: [{ message: { type: String, maxlength: 10 } }] }); // Update validators won't check this, so you can still `$push` 2 elements// onto the array, so long as they don't have a `message` that's too long. testSchema.path('arr').validate(function(v) { return v.length < 2; }); constTest = db.model('Test', testSchema); let update = { $inc: { number: 1 } }; const opts = { runValidators: true }; // There will never be a validation error hereawaitTest.updateOne({}, update, opts); // This will never error either even though the array will have at// least 2 elements. update = { $push: [{ message: 'hello' }, { message: 'world' }] }; awaitTest.updateOne({}, update, opts);
Now that we've covered Validation
, let's take a look at Middleware.