验证

¥Validation

在我们讨论验证语法的细节之前,请记住以下规则:

¥Before we get into the specifics of validation syntax, please keep the following rules in mind:

  • 验证在 SchemaType 中定义

    ¥Validation is defined in the SchemaType

  • 验证为 中间件。默认情况下,Mongoose 在每个结构上将验证注册为 pre('save') 钩子。

    ¥Validation is middleware. Mongoose registers validation as a pre('save') hook on every schema by default.

  • 验证始终作为第一个 pre('save') 钩子运行。这意味着验证不会对你在 pre('save') 钩子中所做的任何更改运行。

    ¥Validation always runs as the first pre('save') hook. This means that validation doesn't run on any changes you make in pre('save') hooks.

  • 你可以通过设置 validateBeforeSave 选项来禁用保存前的自动验证

    ¥You can disable automatic validation before save by setting the validateBeforeSave option

  • 你可以使用 doc.validate()doc.validateSync() 手动运行验证

    ¥You can manually run validation using doc.validate() or doc.validateSync()

  • 你可以使用 doc.invalidate(...) 手动将字段标记为无效(导致验证失败)

    ¥You can manually mark a field as invalid (causing validation to fail) by using doc.invalidate(...)

  • 验证器不会在未定义的值上运行。唯一的例外是 required 验证器

    ¥Validators are not run on undefined values. The only exception is the required validator.

  • 当你调用 Model#save 时,Mongoose 还会运行子文档验证。如果发生错误,你的 Model#save promise 将被拒绝

    ¥When you call Model#save, Mongoose also runs subdocument validation. If an error occurs, your Model#save promise rejects

  • 验证是可定制的

    ¥Validation is customizable

const schema = new Schema({
  name: {
    type: String,
    required: true
  }
});
const Cat = db.model('Cat', schema);

// This cat has no name :(
const cat = new Cat();

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.');

内置验证器

¥Built-in Validators

Mongoose 有几个内置的验证器。

¥Mongoose has several built-in validators.

上面的每个验证器链接都提供了有关如何启用它们并自定义其错误消息的更多信息。

¥Each of the validator links above provide more information about how to enable them and customize their error messages.

const breakfastSchema = new Schema({
  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() {
      return this.bacon > 3;
    }
  }
});
const Breakfast = db.model('Breakfast', breakfastSchema);

const badBreakfast = new Breakfast({
  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?');

自定义错误消息

¥Custom Error Messages

你可以为结构中的各个验证器配置错误消息。有两种等效的方法来设置验证器错误消息:

¥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}']

    ¥Array syntax: min: [6, 'Must be at least 6, got {VALUE}']

  • 对象语法:enum: { values: ['Coffee', 'Tea'], message: '{VALUE} is not supported' }

    ¥Object syntax: enum: { values: ['Coffee', 'Tea'], message: '{VALUE} is not supported' }

Mongoose 还支持错误消息的基本模板。Mongoose 将 {VALUE} 替换为正在验证的值。

¥Mongoose also supports rudimentary templating for error messages. Mongoose replaces {VALUE} with the value being validated.

const breakfastSchema = new Schema({
  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'
    }
  }
});
const Breakfast = db.model('Breakfast', breakfastSchema);

const badBreakfast = new Breakfast({
  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 选项不是验证器

¥The unique Option is Not a Validator

初学者的一个常见问题是结构的 unique 选项不是验证器。是搭建 MongoDB 唯一索引 的便捷帮手。请参阅 常见问题 了解更多信息。

¥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 = new Schema({
  username: {
    type: String,
    unique: true
  }
});
const U1 = db.model('U1', uniqueUsernameSchema);
const U2 = 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);
  });

自定义验证器

¥Custom Validators

如果内置验证器不够,你可以定义自定义验证器来满足你的需求。

¥If the built-in validators aren't enough, you can define custom validators to suit your needs.

自定义验证是通过传递验证函数来声明的。你可以在 SchemaType#validate() API 文档 中找到有关如何执行此操作的详细说明。

¥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 = new Schema({
  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']
  }
});

const User = db.model('user', userSchema);
const user = new User();
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);

异步自定义验证器

¥Async Custom Validators

自定义验证器也可以是异步的。如果你的验证器函数返回一个 promise(如 async 函数),Mongoose 将等待该 promise 解决。如果返回的 Promise 被拒绝,或者满足 false 值,Mongoose 会认为这是一个验证错误。

¥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 = new Schema({
  name: {
    type: String,
    // You can also make a validator async by returning a promise.
    validate: () => Promise.reject(new Error('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'
    }
  }
});

const User = db.model('User', userSchema);
const user = new User();

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');

验证错误

¥Validation Errors

验证失败后返回的错误包含 errors 对象,其值为 ValidatorError 对象。每个 ValidatorError 都有 kindpathvaluemessage 属性。ValidatorError 还可能具有 reason 属性。如果验证器中抛出错误,则此属性将包含抛出的错误。

¥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 = new Schema({
  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') {
    throw new Error('Need to get a Turbo Man for Christmas');
  }
  return true;
}, 'Name `{VALUE}` is not valid');

const Toy = db.model('Toy', toySchema);

const toy = new Toy({ 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');

角色错误

¥Cast Errors

在运行验证器之前,Mongoose 会尝试将值强制为正确的类型。此过程称为转换文档。如果给定路径的转换失败,则 error.errors 对象将包含 CastError 对象。

¥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.

转换在验证之前运行,如果转换失败,则验证不会运行。这意味着你的自定义验证器可能会假定 vnullundefined 或结构中指定类型的实例。

¥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 }
});
const Vehicle = db.model('Vehicle', vehicleSchema);

const doc = new Vehicle({ 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;

默认情况下,Mongoose 转换错误消息看起来像 Cast to Number failed for value "pie" at path "numWheels"。你可以通过 SchemaType 上的 cast 选项将 Mongoose 的默认转换错误消息覆盖为字符串,如下所示。

¥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'
  }
});
const Vehicle = db.model('Vehicle', vehicleSchema);

const doc = new Vehicle({ numWheels: 'pie' });
const err = doc.validateSync();

err.errors['numWheels'].name; // 'CastError'
// "pie" is not a number
err.errors['numWheels'].message;

Mongoose 的强制转换错误消息模板支持以下参数:

¥Mongoose's cast error message templating supports the following parameters:

  • {PATH}:投射失败的路径

    ¥{PATH}: the path that failed to cast

  • {VALUE}:转换失败的值的字符串表示形式

    ¥{VALUE}: a string representation of the value that failed to cast

  • {KIND}:Mongoose 尝试转换的类型,例如 'String''Number'

    ¥{KIND}: the type that Mongoose attempted to cast to, like 'String' or 'Number'

你还可以定义一个函数,Mongoose 将调用该函数来获取转换错误消息,如下所示。

¥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`]
  }
});
const Vehicle = db.model('Vehicle', vehicleSchema);

const doc = new Vehicle({ numWheels: 'pie' });
const err = doc.validateSync();

err.errors['numWheels'].name; // 'CastError'
// "pie" is not a number
err.errors['numWheels'].message;

全局 SchemaType 验证

¥Global SchemaType Validation

除了在各个结构路径上定义自定义验证器之外,你还可以配置自定义验证器以在给定 SchemaType 的每个实例上运行。例如,以下代码演示了如何使空字符串 '' 对所有字符串路径都成为无效值。

¥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 = new Schema({
  name: String,
  email: String
});
const User = db.model('User', userSchema);

const user = new User({ name: '', email: '' });

const err = await user.validate().then(() => null, err => err);
err.errors['name']; // ValidatorError
err.errors['email']; // ValidatorError

嵌套对象所需的验证器

¥Required Validators On Nested Objects

在 mongoose 中的嵌套对象上定义验证器很棘手,因为嵌套对象不是完全成熟的路径。

¥Defining validators on nested objects in mongoose is tricky, because nested objects are not fully fledged paths.

let personSchema = new Schema({
  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 schema
const nameSchema = new Schema({
  first: String,
  last: String
});

personSchema = new Schema({
  name: {
    type: nameSchema,
    required: true
  }
});

const Person = db.model('Person', personSchema);

const person = new Person();
const error = person.validateSync();
assert.ok(error.errors['name']);

更新验证器

¥Update Validators

在上面的示例中,你了解了文档验证。Mongoose 还支持 update()updateOne()updateMany()findOneAndUpdate() 操作的验证。更新验证器默认关闭 - 你需要指定 runValidators 选项。

¥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.

要打开更新验证器,请将 runValidators 选项设置为 update()updateOne()updateMany()findOneAndUpdate()。当心:更新验证器默认处于关闭状态,因为它们有几个注意事项。

¥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 = new Schema({
  color: String,
  name: String
});

const Toy = 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 {
  await Toy.updateOne({}, { color: 'not a color' }, opts);
} catch (err) {
  error = err;
}

assert.equal(error.errors.color.message, 'Invalid color');

更新验证器和 this

¥Update Validators and this

更新验证器和文档验证器之间存在一些关键区别。在下面的颜色验证功能中,this 指的是使用文档验证时正在验证的文档。但是,当运行更新验证器时,this 引用查询对象而不是文档。因为查询有一个简洁的 .get() 函数,所以你可以获得所需属性的更新值。

¥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 = new Schema({
  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';
  }
  return true;
});

const Toy = db.model('ActionFigure', toySchema);

const toy = new Toy({ 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 {
  await Toy.updateOne({}, update, opts);
} catch (err) {
  error = err;
}
// Validation failed: color: Validator failed for path `color` with value `green`
assert.ok(error);

更新验证器仅在更新的路径上运行

¥Update Validators Only Run On Updated Paths

另一个关键区别是更新验证器仅在更新中指定的路径上运行。例如,在下面的示例中,由于更新操作中未指定 'name',因此更新验证将成功。

¥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.

使用更新验证器时,只有当你尝试显式对密钥进行 $unset 操作时,required 验证器才会失败。

¥When using update validators, required validators only fail when you try to explicitly $unset the key.

const kittenSchema = new Schema({
  name: { type: String, required: true },
  age: Number
});

const Kitten = db.model('Kitten', kittenSchema);

const update = { color: 'blue' };
const opts = { runValidators: true };
// Operation succeeds despite the fact that 'name' is not specified
await Kitten.updateOne({}, update, opts);

const unset = { $unset: { name: 1 } };
// Operation fails because 'name' is required
const err = await Kitten.updateOne({}, unset, opts).then(() => null, err => err);
assert.ok(err);
assert.ok(err.errors['name']);

更新验证器仅针对某些操作运行

¥Update Validators Only Run For Some Operations

最后一个值得注意的细节:更新验证器仅在以下更新运算符上运行:

¥One final detail worth noting: update validators only run on the following update operators:

  • $set

  • $unset

  • $push

  • $addToSet

  • $pull

  • $pullAll

例如,无论 number 的值如何,以下更新都会成功,因为更新验证器会忽略 $inc

¥For instance, the below update will succeed, regardless of the value of number, because update validators ignore $inc.

此外,$push$addToSet$pull$pullAll 验证不会对数组本身运行任何验证,仅对数组的各个元素运行。

¥Also, $push, $addToSet, $pull, and $pullAll validation does not run any validation on the array itself, only individual elements of the array.

const testSchema = new Schema({
  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;
});

const Test = db.model('Test', testSchema);

let update = { $inc: { number: 1 } };
const opts = { runValidators: true };

// There will never be a validation error here
await Test.updateOne({}, update, opts);

// This will never error either even though the array will have at
// least 2 elements.
update = { $push: [{ message: 'hello' }, { message: 'world' }] };
await Test.updateOne({}, update, opts);

下一步

¥Next Up

现在我们已经介绍了 Validation,让我们来看看 中间件

¥Now that we've covered Validation, let's take a look at Middleware.