mongooseでスキーマにないフィールドを指定したらどうなる?知らんのか

スキーマレスが特徴のMongoDBを、ODMのmongoose経由で使う機会がありました。
「mongooseを使う=スキーマを定義する」ってことですが、存在しないフィールドの扱いがどうなるのか気になったので検証しました。

検証環境

バージョンは次のとおり。

検証コード

↓コード詳細
import mongoose from "mongoose";
import { MongoMemoryServer } from "mongodb-memory-server";
// スキーマ
const UserSchema = new mongoose.Schema(
{
name: String,
},
// { strict: true },
);
type UserModel = mongoose.Model<mongoose.InferSchemaType<typeof UserSchema>>;
// create検証
async function testCreate(User: UserModel) {
console.log("--- create 検証 ---");
await User.create({
name: "eetann",
age: 99, // スキーマにないフィールド
});
console.log("After create:", await User.findOne({ name: "eetann" }));
}
// insertMany検証
async function testInsertMany(User: UserModel) {
console.log("--- insertMany 検証 ---");
await User.insertMany([
{ name: "Taro", hobby: "reading" }, // スキーマにないフィールド
{ name: "Ichiro", score: 100 }, // スキーマにないフィールド
]);
console.log("After insertMany:", await User.find({}));
}
// updateOne 検証
async function testUpdateOne(User: UserModel) {
console.log("--- updateOne 検証 ---");
await User.updateOne({ name: "eetann" }, { $set: { country: "Japan" } });
console.log("After updateOne:", await User.findOne({ name: "eetann" }));
}
// findOneAndUpdate 検証
async function testFindOneAndUpdate(User: UserModel) {
console.log("--- findOneAndUpdate 検証 ---");
const updated = await User.findOneAndUpdate(
{ name: "Taro" },
{ $set: { level: 5 } },
{ new: true },
);
console.log("After findOneAndUpdate:", updated);
}
// new + saveの検証
async function testNewAndSave(User: UserModel) {
console.log("--- new + save 検証 ---");
const newUser = new User({ name: "Hanako", status: "active" }); // スキーマにないフィールド
await newUser.save();
console.log("After new + save:", newUser);
}
// 既存ドキュメントにスキーマにないフィールドを代入して save
async function testModifyAndSave(User: UserModel) {
console.log("--- modify + save 検証 ---");
const existingUser = await User.findOne({ name: "Ichiro" });
if (existingUser) {
// 型エラーになる
existingUser.role = "admin"; // スキーマにないフィールド
await existingUser.save();
console.log("After modify + save:", existingUser);
}
}
// toObject / toJSON 検証
async function testToObjectAndToJSON(User: UserModel) {
console.log("--- toObject() / toJSON() 検証 ---");
const userForConvert = await User.findOne({ name: "eetann" });
if (userForConvert) {
userForConvert.name = "eetann_updated"; // 存在するフィールドに代入
// 型エラーになる
userForConvert.editor = "neovim"; // 存在しないフィールドに代入
// save無し
console.log("直接代入後 toObject():", userForConvert.toObject());
console.log("直接代入後 toJSON():", userForConvert.toJSON());
console.log("直接代入後 JSON.stringify():", JSON.stringify(userForConvert));
}
}
// main関数
async function main() {
console.log(`Mongoose version: ${mongoose.version}`);
// In-memory MongoDB 起動
const mem = await MongoMemoryServer.create();
const uri = mem.getUri();
console.log("MongoDB URI:", uri);
// 接続
await mongoose.connect(uri);
const User = mongoose.model("User", UserSchema);
// 各検証を実行
await testCreate(User);
await testInsertMany(User);
await testUpdateOne(User);
await testFindOneAndUpdate(User);
await testNewAndSave(User);
await testModifyAndSave(User);
await testToObjectAndToJSON(User);
// 結果確認
console.log("--- Final result ---");
const data = await User.find({});
console.log("Final result:", data);
// 終了処理
await mongoose.disconnect();
await mem.stop();
}
main().catch((err) => console.error(err));

筆者はBunを使って実行しました。

Terminal window
bun run no-exist-field.ts
↓実行結果
Mongoose version: 9.0.1
MongoDB URI: mongodb://127.0.0.1:50273/
--- create 検証 ---
After create: {
_id: new ObjectId('693d5635d9d42be0d197ba7e'),
name: "eetann",
__v: 0,
}
--- insertMany 検証 ---
After insertMany: [
{
_id: new ObjectId('693d5635d9d42be0d197ba7e'),
name: "eetann",
__v: 0,
}, {
_id: new ObjectId('693d5635d9d42be0d197ba81'),
name: "Taro",
__v: 0,
}, {
_id: new ObjectId('693d5635d9d42be0d197ba82'),
name: "Ichiro",
__v: 0,
}
]
--- updateOne 検証 ---
After updateOne: {
_id: new ObjectId('693d5635d9d42be0d197ba7e'),
name: "eetann",
__v: 0,
}
--- findOneAndUpdate 検証 ---
After findOneAndUpdate: {
_id: new ObjectId('693d5635d9d42be0d197ba81'),
name: "Taro",
__v: 0,
}
--- new + save 検証 ---
After new + save: {
name: "Hanako",
_id: new ObjectId('693d5635d9d42be0d197ba87'),
__v: 0,
}
--- modify + save 検証 ---
After modify + save: {
_id: new ObjectId('693d5635d9d42be0d197ba82'),
name: "Ichiro",
__v: 0,
}
--- toObject() / toJSON() 検証 ---
直接代入後 toObject(): {
_id: new ObjectId('693d5635d9d42be0d197ba7e'),
name: "eetann_updated",
__v: 0,
}
直接代入後 toJSON(): {
_id: new ObjectId('693d5635d9d42be0d197ba7e'),
name: "eetann_updated",
__v: 0,
}
直接代入後 JSON.stringify(): {"_id":"693d5635d9d42be0d197ba7e","name":"eetann_updated","__v":0}
--- Final result ---
Final result: [
{
_id: new ObjectId('693d5635d9d42be0d197ba7e'),
name: "eetann",
__v: 0,
}, {
_id: new ObjectId('693d5635d9d42be0d197ba81'),
name: "Taro",
__v: 0,
}, {
_id: new ObjectId('693d5635d9d42be0d197ba82'),
name: "Ichiro",
__v: 0,
}, {
_id: new ObjectId('693d5635d9d42be0d197ba87'),
name: "Hanako",
__v: 0,
}
]

検証結果

一言で書くと スキーマにないフィールドは無視されます 。これはmongooseのデフォルト設定がstrict: trueになっているからです。

具体的に見ていきましょう。

作成・更新系のメソッド

createinsertManyupdateOnefindOneAndUpdateは普通に無視されます。

await User.create({
name: "eetann",
age: 99, // スキーマにないフィールド
});

new + save

newしてからsaveするやつでも同様に、スキーマの無いやつは無視されます。これも想定どおりだと思います。

const newUser = new User({
name: "Hanako",
status: "active" // スキーマにないフィールド
});
await newUser.save();

途中から代入するパターン

〇〇.未定義フィールド = 値のように、途中で代入するパターンも試しました。同様に無視されます。

const user = await User.findOne({ name: "Ichiro" });
user.role = "admin"; // スキーマにないフィールド
await user.save();

TypeScriptを使ってれば型エラーになるので問題ないですが、 JavaScriptを使っていると気づけません

toObject・toJSON

toObject()toJSON()は、存在するフィールドなら更新が反映され、存在しないなら無視されます。

const user = await User.findOne({ name: "eetann" });
user.name = "eetann_updated"; // 存在するフィールドに代入
user.role = "admin"; // スキーマにないフィールド
const hoge = {
...user.toObject(), // nameは更新・roleは含まれない
hoge: 1,
}
console.log(user.toJSON()) // nameは更新・roleは含まれない

もちろんsaveしなければ、存在するフィールドの更新もDBには反映されません。

strictオプションで制御

公式ドキュメントに載っているとおりstrictオプションで制御できます。

まぁわざわざmongooseを使っている時点で「strict: falseにする人」はよほどの事情を抱えている少数派だと思います。

まとめ

デフォルトだと無視するけどJavaScriptだと分かりづらいので気を付けましょう。


以上、Mongooseの未定義フィールドの挙動でした。