エラーの内容
BSONTypeError: Argument passed in must be a string of 12 bytes or a string of 24 hex characters or an integer
このエラーは、new ObjectId() に無効な値を渡したときに発生します。よくある原因として、undefined、空文字列、またはユーザー入力や不正なURLパラメータから届いた壊れたIDが挙げられます。
なぜこのエラーが起きるのか
MongoDB の ObjectId は、正確に24文字の16進数(例:507f1f77bcf86cd799439011)または12バイトの生データである必要があります。BSONはこの仕様に厳格で、それ以外のものを渡すと即座に例外をスローします。部分的なマッチは一切許可されません。
よくあるトリガー:
- ルートのパラメータ名と読み取っているプロパティ名が一致しないため、ルートパラメータが
undefinedになっている - クライアントが正規の ObjectId ではなく、無効な文字列や短いIDを送信している
- ObjectId の文字列が転送中に切り詰められた — たった1文字欠けるだけでエラーになる
- mongodb@4以降のプロジェクトで
ObjectID(大文字のD、mongodb@3スタイル)をインポートしている — 暗黙のうちにundefinedが返される - すでに ObjectId インスタンスであるドキュメントの
_idをnew ObjectId()に再度渡している — 冗長だが通常は無害
修正手順
1. ObjectId を作成する前に必ずバリデーションを行う
BSONにはまさにこのための ObjectId.isValid() が用意されています。ルートパラメータ、リクエストボディ、クエリ文字列など、コード外部からIDが来るたびに必ず呼び出してください。
const { ObjectId } = require('mongodb'); // または 'bson' から
// NG — id が undefined または不正な形式の場合にクラッシュする
const id = new ObjectId(req.params.id);
// OK — まずバリデーションを行う
const rawId = req.params.id;
if (!ObjectId.isValid(rawId)) {
return res.status(400).json({ error: 'Invalid ID format' });
}
const id = new ObjectId(rawId);
2. インポートを確認する — ObjectId と ObjectID
mongodb@3 では ObjectID(大文字のD)としてエクスポートされていました。mongodb@4 からは ObjectId(小文字のd)に変更されました。誤った名前でインポートすると undefined が返され、new undefined() でエラーが発生します。
// mongodb@4+ (現行)
const { ObjectId } = require('mongodb');
// ESM
import { ObjectId } from 'mongodb';
// Mongoose
const { Types } = require('mongoose');
const id = new Types.ObjectId(rawId);
// まだ mongodb@3 を使用中?大文字のD:
const { ObjectID } = require('mongodb');
3. 「パラメータ名の間違い」という典型的なケースを修正する
ルートが /api/users/:userId なのに req.params.id を読み取っている場合、結果は undefined になります。これは非常によくあるミスです。パラメータ名が一貫して一致しているかどうか、端から端まで確認してください。
// ルート: GET /api/users/:userId
router.get('/api/users/:userId', async (req, res) => {
const { userId } = req.params; // req.params.id ではない
if (!userId || !ObjectId.isValid(userId)) {
return res.status(400).json({ error: 'Invalid or missing user ID' });
}
const user = await db.collection('users').findOne({ _id: new ObjectId(userId) });
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
});
4. バルク操作では try-catch で囲む
バッチで50件のIDを処理する場合、1つの不正なエントリでリクエスト全体を失敗させるべきではありません。小さなヘルパー関数で不正なものを除外しながら処理を続けましょう:
function toObjectId(id) {
try {
return new ObjectId(id);
} catch {
return null;
}
}
const ids = rawIds.map(toObjectId).filter(Boolean);
const results = await db.collection('items').find({ _id: { $in: ids } }).toArray();
5. isValid() の12文字文字列に関する注意点
ObjectId.isValid() は16進数だけでなく、あらゆる12文字の文字列に対して true を返します。試してみてください:ObjectId.isValid("hello world!") は true になります。厳密な24文字16進数バリデーションが必要なユーザー向けIDには、正規表現を追加してください:
function isStrictObjectId(id) {
return typeof id === 'string' && /^[a-f\d]{24}$/i.test(id);
}
// 使用例
if (!isStrictObjectId(req.params.id)) {
return res.status(400).json({ error: 'Invalid ID' });
}
修正の確認
次のクイックチェックを実行してください。各出力がインラインコメントと一致するはずです:
const { ObjectId } = require('mongodb');
// 有効 — 正常に動作するはず
console.log(ObjectId.isValid('507f1f77bcf86cd799439011')); // true
console.log(new ObjectId('507f1f77bcf86cd799439011').toString()); // '507f1f77bcf86cd799439011'
// 無効 — ObjectId() が実行される前に isValid() がキャッチする
console.log(ObjectId.isValid(undefined)); // false
console.log(ObjectId.isValid('')); // false
console.log(ObjectId.isValid('123')); // false
console.log(ObjectId.isValid('not-hex-data')); // false
6つの出力がすべて一致していれば、バリデーションは完璧です。
ヒント
- チェックを一元化する: 共有のユーティリティファイルに
isValidObjectId()ヘルパーを1つ作成しましょう。数十のルートハンドラに同じ3行をコピペするより、1か所でインポートする方が遥かに優れています。 - 境界でバリデーションを行う: サービス関数の奥深くではなく、ルートハンドラでIDをチェックしましょう。早期にキャッチすることで、エラーメッセージがより明確になり、デバッグ時間も半減します。
- 不正な値をログに残す: チェックが失敗した場合は、何が渡ってきたかを正確にログに記録しましょう —
console.error('Invalid ObjectId:', rawId)。クライアントがリテラル文字列として"undefined"を送ってきたとき、このログに初めて気づいて助かるでしょう。 - Mongoose の CastError: 無効なIDを
Model.findById(id)に渡すと、Mongoose はBSONTypeErrorではなくCastErrorをスローします。エラー名は異なりますが、根本原因は同じです。どちらの場合もクエリ前にバリデーションを行ってください。 - 既存の ObjectId を再ラップしない:
doc._idがすでに ObjectId インスタンスであれば、new ObjectId(doc._id)は技術的には動作しますが、無駄なオーバーヘッドが生じます。クエリではそのインスタンスを直接使用してください。

