Mongooseでフィールドを削除したオブジェクトを使うときは注意

最近mongooseMongoDBのODM)と格闘中です。DBから引っ張ったデータを 一部のフィールドを削除してからレスポンスとして渡すとき に、 「消したつもりなのに消えてない……!」 となったので備忘録です。

結論からいうとdelete 〇〇.フィールド名では消えないので、他の方法を使う必要があります。

## 検証環境

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

今回はレスポンスで返すことを想定しているため、〇〇.save()などは出てきません。

## 検証コード

↓コード詳細
import mongoose from "mongoose";
import { MongoMemoryServer } from "mongodb-memory-server";
// スキーマ
const UserSchema = new mongoose.Schema({
name: String,
email: String,
password: String, // 外部に渡すときは削除したいフィールド
});
type UserModel = mongoose.Model<mongoose.InferSchemaType<typeof UserSchema>>;
async function testDeleteOperatorNotWorking(User: UserModel) {
console.log("=== ❌ BAD: delete演算子は効かない ===");
const user = await User.findOne({ name: "eetann" });
if (!user) return;
delete user.password;
console.log("delete後のuser.password:", user.password); // まだ値がある!
console.log("delete後 toObject():", user.toObject());
console.log("delete後 toJSON():", user.toJSON());
console.log("");
}
async function testAssignUndefined(User: UserModel) {
console.log("=== ⚠️ WORKS: undefined代入は効く(deny方式) ===");
const user = await User.findOne({ name: "eetann" });
if (!user) return;
user.password = undefined;
console.log("undefined代入後 toObject():", user.toObject());
console.log("");
}
async function testDeleteAfterToObject(User: UserModel) {
console.log("=== ⚠️ WORKS: toObject()してからdelete(deny方式) ===");
const user = await User.findOne({ name: "eetann" });
if (!user) return;
const userObj = user.toObject();
delete userObj.password;
console.log("toObject()後にdelete:", userObj);
console.log("");
}
async function testSelectExcludeFields(User: UserModel) {
console.log("=== ⚠️ WORKS: select()で除外フィールド指定(deny方式) ===");
const user = await User.findOne({ name: "eetann" }).select("-password -__v");
console.log("select()で除外:", user?.toObject());
console.log("");
}
async function testSelectFields(User: UserModel) {
console.log(
"=== ✅ GOOD: select()で必要なフィールドだけ取得(allow方式) ===",
);
const user = await User.findOne({ name: "eetann" }).select("name email");
console.log("select()で取得:", user?.toObject());
console.log("");
}
async function testProjection(User: UserModel) {
console.log("=== ✅ GOOD: projection でフィールド指定(allow方式) ===");
const user = await User.findOne({ name: "eetann" }, { name: 1, email: 1 });
console.log("projection指定:", user?.toObject());
console.log("");
}
async function testPickFields(User: UserModel) {
console.log("=== ✅ GOOD: 必要なフィールドだけピック(allow方式) ===");
const user = await User.findOne({ name: "eetann" });
if (!user) return;
const safeUser = {
name: user.name,
email: user.email,
};
console.log("明示的に構築:", safeUser);
console.log("");
}
// main関数
async function main() {
console.log(`Mongoose version: ${mongoose.version}`);
console.log("");
// In-memory MongoDB 起動
const mem = await MongoMemoryServer.create();
const uri = mem.getUri();
// 接続
await mongoose.connect(uri);
const User = mongoose.model("User", UserSchema);
// テストデータ作成
await User.create({
name: "eetann",
email: "eetann@example.com",
password: "super_secret_password",
});
// 各検証を実行
// deny
await testDeleteOperatorNotWorking(User);
await testAssignUndefined(User);
await testDeleteAfterToObject(User);
await testSelectExcludeFields(User);
// allow
await testSelectFields(User);
await testPickFields(User);
await testProjection(User);
// 終了処理
await mongoose.disconnect();
await mem.stop();
}
main().catch((err) => console.error(err));

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

Terminal window
bun run delete-property.ts
↓実行結果
Mongoose version: 9.0.1
=== ❌ BAD: delete演算子は効かない ===
delete後のuser.password: super_secret_password
delete後 toObject(): {
_id: new ObjectId('6972bb2286e81e016e5c8aa3'),
name: "eetann",
email: "eetann@example.com",
password: "super_secret_password",
__v: 0,
}
delete後 toJSON(): {
_id: new ObjectId('6972bb2286e81e016e5c8aa3'),
name: "eetann",
email: "eetann@example.com",
password: "super_secret_password",
__v: 0,
}
=== ⚠️ WORKS: undefined代入は効く(deny方式) ===
undefined代入後 toObject(): {
_id: new ObjectId('6972bb2286e81e016e5c8aa3'),
name: "eetann",
email: "eetann@example.com",
__v: 0,
}
=== ⚠️ WORKS: toObject()してからdelete(deny方式) ===
toObject()後にdelete: {
_id: new ObjectId('6972bb2286e81e016e5c8aa3'),
name: "eetann",
email: "eetann@example.com",
__v: 0,
}
=== ⚠️ WORKS: select()で除外フィールド指定(deny方式) ===
select()で除外: {
_id: new ObjectId('6972bb2286e81e016e5c8aa3'),
name: "eetann",
email: "eetann@example.com",
}
=== ✅ GOOD: select()で必要なフィールドだけ取得(allow方式) ===
select()で取得: {
_id: new ObjectId('6972bb2286e81e016e5c8aa3'),
name: "eetann",
email: "eetann@example.com",
}
=== ✅ GOOD: 必要なフィールドだけピック(allow方式) ===
明示的に構築: {
name: "eetann",
email: "eetann@example.com",
}
=== ✅ GOOD: projection でフィールド指定(allow方式) ===
projection指定: {
_id: new ObjectId('6972bb2286e81e016e5c8aa3'),
name: "eetann",
email: "eetann@example.com",
}

## delete演算子は効かない

delete 〇〇.フィールド名のようなプロパティ削除は効きません。

const user = await User.findOne({ name: "eetann" });
delete user.password;
console.log("delete後のuser.password:", user.password); // まだ値がある!
console.log("delete後 toObject():", user.toObject());
console.log("delete後 toJSON():", user.toJSON());

純粋なJavaScriptのオブジェクトではないのでまあ納得です。

## deny方式:除外する

実際に使える「指定したフィールドを消す方法」をいくつか載せます。

### undefined代入

〇〇.フィールド名 = undefinedのように代入すると消せます。

user.password = undefined;

これはレスポンスに渡す前に限らず、普通にフィールドのデータを削除してDBに保存するときにも使うやり方ですね。

### toObject()してからdelete

オブジェクトに変換してからであれば、deleteしても消えます。

const userObj = user.toObject();
delete userObj.password;

### select()で除外指定

使う機会は少なそうですが、selectでマイナスを指定しても消せます。

const user = await User.findOne({ name: "eetann" }).select("-password");

## allow方式:必要なものだけを渡す

そもそも必要なフィールドだけを取得しているか見直し、レスポンスでも必要なフィールドだけを渡すようにしましょう。

### DBから引っ張る段階で必要なフィールドだけ取得

まず取得段階で必要なやつだけを引っ張ってきます。

// どっちでもいい
const user = await User.findOne({ name: "eetann" }, { name: 1, email: 1 });
const user = await User.findOne({ name: "eetann" }).select("name email");

### あとから必要なやつだけ渡す

で、レスポンスに渡す前に必要なやつだけにまとめます。

const safeUser = {
name: user.name,
email: user.email,
};

## allowの方がいいぞ

deny方式だと将来フィールドを追加したときに消し忘れたりどんどん肥大化するリスクがあります。なるべく必要なやつだけを明示的に渡すようにしましょう。

とはいえ、元がdeny方式のコードを後から変えようとすると影響範囲調べるの大変ですよね。分かります……。

## 関連記事

今回とは逆に「スキーマにないフィールドを追加しようとしたらどうなるか」を検証した記事も書いてます。興味があればどうぞ。

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


以上、mongooseのフィールド削除の扱いでした。