バリデーション
バリデーションの構文の詳細に入る前に、次のルールを念頭に置いてください。
- バリデーションはSchemaTypeで定義されます
- バリデーションはミドルウェアです。Mongooseは、デフォルトですべてのスキーマに
pre('save')
フックとしてバリデーションを登録します。 - バリデーションは、常に最初の
pre('save')
フックとして実行されます。つまり、バリデーションは、pre('save')
フックで行う変更に対しては実行されません。 - validateBeforeSaveオプションを設定することで、保存前の自動バリデーションを無効にできます。
doc.validate()
またはdoc.validateSync()
を使用して、手動でバリデーションを実行できます。doc.invalidate(...)
を使用すると、手動でフィールドを無効としてマーク(バリデーションを失敗させる)できます。- バリデーターは、undefined値では実行されません。唯一の例外は、
required
バリデーターです。 - Model#saveを呼び出すと、Mongooseはサブドキュメントのバリデーションも実行します。エラーが発生した場合、Model#saveのPromiseは拒否されます。
- バリデーションはカスタマイズ可能です。
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.');
- 組み込みバリデーター
- カスタムエラーメッセージ
unique
オプションはバリデーターではありません- カスタムバリデーター
- 非同期カスタムバリデーター
- バリデーションエラー
- キャストエラー
- グローバルSchemaTypeバリデーション
- ネストされたオブジェクトに対する必須バリデーター
- 更新バリデーター
- 更新バリデーターと
this
- 更新バリデーターは更新されたパスでのみ実行されます
- 更新バリデーターは一部の操作でのみ実行されます
組み込みバリデーター
Mongooseには、いくつかの組み込みバリデーターがあります。
- すべてのSchemaTypeには、組み込みのrequiredバリデーターがあります。requiredバリデーターは、値がrequiredバリデーターを満たしているかどうかを判断するために、SchemaTypeの
checkRequired()
関数を使用します。 - 数値には、
min
とmax
バリデーターがあります。 - 文字列には、
enum
、match
、minLength
、およびmaxLength
バリデーターがあります。
上記の各バリデーターリンクは、それらを有効にする方法とエラーメッセージをカスタマイズする方法に関する詳細情報を提供します。
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?');
カスタムエラーメッセージ
スキーマ内の個々のバリデーターのエラーメッセージを設定できます。バリデーターエラーメッセージを設定するには、同等の2つの方法があります。
- 配列構文:
min: [6, 'Must be at least 6, got {VALUE}']
- オブジェクト構文:
enum: { values: ['Coffee', 'Tea'], message: '{VALUE} is not supported' }
Mongooseは、エラーメッセージの基本的なテンプレートもサポートしています。Mongooseは、{VALUE}
を検証中の値に置き換えます。
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
オプションはバリデーターではありません
初心者によくある落とし穴は、スキーマのunique
オプションはバリデーターではないということです。これは、MongoDBの一意なインデックスを構築するための便利なヘルパーです。詳細については、FAQを参照してください。
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);
});
カスタムバリデーター
組み込みのバリデーターでは不十分な場合は、ニーズに合わせてカスタムバリデーターを定義できます。
カスタムバリデーションは、バリデーション関数を渡すことによって宣言されます。これを行う方法の詳細な手順については、SchemaType#validate()
APIドキュメントを参照してください。
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);
非同期カスタムバリデーター
カスタムバリデーターは非同期にすることもできます。バリデーター関数がPromise(async
関数のように)を返す場合、MongooseはそのPromiseが解決するのを待ちます。返されたPromiseが拒否された場合、または値false
で履行された場合、Mongooseはそれをバリデーションエラーと見なします。
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');
バリデーションエラー
バリデーションの失敗後に返されるエラーには、値がValidatorError
オブジェクトであるerrors
オブジェクトが含まれます。各ValidatorErrorには、kind
、path
、value
、およびmessage
プロパティがあります。ValidatorErrorにはreason
プロパティも含まれる場合があります。バリデーターでエラーがスローされた場合、このプロパティにはスローされたエラーが含まれます。
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');
キャストエラー
バリデーターを実行する前に、Mongooseは値を正しい型に強制的に変換しようとします。このプロセスは、ドキュメントのキャスティングと呼ばれます。特定のパスでキャスティングが失敗した場合、error.errors
オブジェクトにはCastError
オブジェクトが含まれます。
キャスティングはバリデーションの前に実行され、キャスティングが失敗した場合、バリデーションは実行されません。つまり、カスタムバリデーターは、v
がnull
、undefined
、またはスキーマで指定された型のインスタンスであると想定できます。
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のデフォルトのキャストエラーメッセージを上書きできます。
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のキャストエラーメッセージテンプレートは、次のパラメーターをサポートしています。
{PATH}
: キャストに失敗したパス{VALUE}
: キャストに失敗した値の文字列表現{KIND}
: Mongooseがキャストしようとした型('String'
や'Number'
など)
次のように、Mongooseがキャストエラーメッセージを取得するために呼び出す関数を定義することもできます。
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バリデーション
個々のスキーマパスでカスタムバリデーターを定義するだけでなく、特定のSchemaType
のすべてのインスタンスで実行するカスタムバリデーターを構成することもできます。たとえば、次のコードは、空の文字列''
をすべての文字列パスに対して無効な値にする方法を示しています。
// 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
ネストされたオブジェクトに対する必須バリデーター
Mongooseでネストされたオブジェクトにバリデーターを定義するのは難しいです。これは、ネストされたオブジェクトが完全なパスではないためです。
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']);
更新バリデーター
上記の例では、ドキュメントのバリデーションについて学習しました。Mongooseは、update()
、updateOne()
、updateMany()
、およびfindOneAndUpdate()
操作のバリデーションもサポートしています。更新バリデーターはデフォルトでオフになっています。runValidators
オプションを指定する必要があります。
更新バリデーターをオンにするには、update()
、updateOne()
、updateMany()
、またはfindOneAndUpdate()
のrunValidators
オプションを設定します。注意: 更新バリデーターにはいくつかの注意点があるため、デフォルトではオフになっています。
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
更新バリデーターとドキュメントバリデーターには、いくつかの重要な違いがあります。以下の色のバリデーション関数では、ドキュメントバリデーションを使用する場合、this
は検証中のドキュメントを参照します。ただし、更新バリデーターを実行する場合、this
はドキュメントではなくクエリオブジェクトを参照します。クエリには便利な.get()
関数があるため、目的のプロパティの更新された値を取得できます。
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);
更新バリデーターは更新されたパスでのみ実行されます
もう1つの重要な違いは、更新バリデーターは更新で指定されたパスでのみ実行されることです。たとえば、以下の例では、'name'が更新操作で指定されていないため、更新バリデーションは成功します。
更新バリデーターを使用する場合、required
バリデーターは、明示的にキーを$unset
しようとした場合にのみ失敗します。
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']);
更新バリデーターは一部の操作でのみ実行されます
最後に注意すべき詳細は、更新バリデーターは次の更新演算子でのみ実行されるということです。
$set
$unset
$push
$addToSet
$pull
$pullAll
たとえば、以下の更新は、更新バリデーターが$inc
を無視するため、number
の値に関係なく成功します。
また、$push
、$addToSet
、$pull
、および$pullAll
のバリデーションは、配列自体のバリデーションは実行せず、配列の個々の要素のみを検証します。
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);
次へ
バリデーション
について説明したので、次はミドルウェアを見てみましょう。