MongoDBエラー修正:$pushや$addToSet使用時の「The path must be an array」エラー

intermediate🍃 MongoDB2026-06-03| MongoDB 4.x / 5.x / 6.x / 7.x、任意のOS(Linux、macOS、Windows)、Mongoose、PyMongo、またはネイティブドライバー使用環境

Error Message

The path 'tags' must be an array in the document, but is of type string
#mongodb#push#addToSet#配列#型不一致#スキーマバリデーション#mongoose#アグリゲーション

何が起きたのか

$push$addToSet を使って配列に値を追加しようとしたところ、MongoDBが処理を止めてしまいました:

MongoServerError: The path 'tags' must be an array in the document, but is of type string

フィールドはドキュメントに存在しているのですが、配列ではなく文字列が格納されています。$push$addToSet は実際の配列にしか機能しません。データを黙って破損させる代わりに、MongoDBは操作全体を拒否します。

30秒で再現する

mongosh を開いて次を実行してください:

// 'tags' が単純な文字列のドキュメントを挿入
db.articles.insertOne({ _id: 1, title: "Hello", tags: "mongodb" })

// 別のタグをプッシュしようとする
db.articles.updateOne({ _id: 1 }, { $push: { tags: "nodejs" } })
// MongoServerError: The path 'tags' must be an array...

フィールドは ["mongodb"] ではなく文字列です。これが問題の本質です。

ステップ1:影響を受けるドキュメントのフィールド型を確認する

何かを変更する前に、実際に格納されているものを確認します:

db.articles.findOne({ _id: 1 }, { tags: 1 })
// { _id: 1, tags: "mongodb" }  ← 配列ではなく文字列

影響を受けるドキュメントが何件あるか分からない場合は、tags が配列でないすべてのドキュメントを検索します:

db.articles.find({
  tags: { $exists: true },
  $expr: { $ne: [{ $type: "$tags" }, "array"] }
})

変更を加える前にこれを実行してください。まず全体像を把握することが重要です。

ステップ2:型が間違っているドキュメントを修正する

ケースA — フィールドに単一の文字列値が入っている場合

既存の文字列を配列でラップしてから、新しい項目を追加します:

// 特定の既知ドキュメントの場合
const doc = db.articles.findOne({ _id: 1 })
const existingTag = doc.tags  // "mongodb"

db.articles.updateOne(
  { _id: 1 },
  { $set: { tags: [existingTag, "nodejs"] } }
)

// 確認
db.articles.findOne({ _id: 1 }, { tags: 1 })
// { _id: 1, tags: [ "mongodb", "nodejs" ] }

ケースB — 影響を受けるドキュメントを一括修正する場合

不正なドキュメントが数百件ある場合は、集計パイプラインによるアップデートを使って、すべてのスカラー値を配列でラップします。アプリ側でのループ処理不要で、一度の操作で完結します:

db.articles.updateMany(
  {
    tags: { $exists: true },
    $expr: { $ne: [{ $type: "$tags" }, "array"] }
  },
  [
    { $set: { tags: ["$tags"] } }  // パイプラインアップデート — 値を [] でラップする
  ]
)

{ $set: ... } を囲む角括弧はタイポではありません。これは集計パイプラインアップデート構文(MongoDB 4.2以降)です。通常のアップデート式ではできない、$set の右辺でドキュメント自身のフィールド値("$tags")を参照できます。

ケースC — フィールドがnull、存在しない、またはその他の非文字列型の場合

フィールドが null だったり、そもそも存在しないこともあります。まず空の配列にリセットしてからプッシュします:

// tagsがまだ配列でないすべてのドキュメントに [] をセット
db.articles.updateMany(
  { $expr: { $ne: [{ $type: "$tags" }, "array"] } },
  { $set: { tags: [] } }
)

// これで $push が正常に動作する
db.articles.updateMany({}, { $push: { tags: "mongodb" } })

ステップ3:再発を防ぐ

オプション1 — JSONスキーマバリデーション(推奨)

スキーマルールを追加して、tags に配列以外の値を書き込もうとした場合にMongoDBが拒否するようにします:

db.runCommand({
  collMod: "articles",
  validator: {
    $jsonSchema: {
      bsonType: "object",
      properties: {
        tags: {
          bsonType: "array",
          items: { bsonType: "string" },
          description: "tags must be an array of strings"
        }
      }
    }
  },
  validationLevel: "moderate"   // "strict" は既存の不正ドキュメントも拒否する
})

これ以降、tags: ["mongodb"] ではなく tags: "mongodb" を送信するインサートは、明確なバリデーションエラーで即座に失敗します。本番環境で $push が突然エラーを出し始めてから何時間も後に気づくのではなく、書き込み時点で捕捉できます。

オプション2 — Mongooseのスキーマ型強制

Mongooseを使用している場合は、スキーマでフィールドを明示的に配列として宣言します:

const articleSchema = new mongoose.Schema({
  title: String,
  tags: { type: [String], default: [] }  // 常に配列
})

const Article = mongoose.model("Article", articleSchema)

Mongooseは保存時に型を強制変換します。また、doc.tags.push("nodejs") のようなMongooseドキュメントメソッドも最初から正しく機能します。生のMongoDBドライバーの呼び出しは不要です。

オプション3 — $each ガードを使った $addToSet の活用

データが正常になったら、タグフィールドには $push より $addToSet を優先して使用してください。配列にすでに存在する値は自動的にスキップされます:

db.articles.updateOne(
  { _id: 1 },
  { $addToSet: { tags: { $each: ["nodejs", "mongodb"] } } }
)

修正が正しく適用されたか確認する

// 1. 特定のドキュメントを確認
db.articles.findOne({ _id: 1 }, { tags: 1 })
// 期待値: { _id: 1, tags: [ "mongodb", "nodejs" ] }

// 2. 配列でないtagsフィールドがもう存在しないことを確認
db.articles.countDocuments({
  tags: { $exists: true },
  $expr: { $ne: [{ $type: "$tags" }, "array"] }
})
// 期待値: 0

// 3. 元の $push を再実行 — 今度は正常に動作するはず
db.articles.updateOne({ _id: 1 }, { $push: { tags: "express" } })
// 期待値: { acknowledged: true, matchedCount: 1, modifiedCount: 1 }

なぜこの問題が繰り返されるのか(そして防ぐ方法)

  • 根本原因は型の不一致です。 $push$addToSet は実際の配列を必要とします。"mongodb" のような単一値の文字列は対象外であり、データを黙って破損させるのではなく、操作全体が失敗します。
  • マイグレーションが主な原因です。 古いバージョンのアプリは1つのタグを単純な文字列として保存していました。後のスキーマ変更で配列による複数タグのサポートが導入されたものの、古いドキュメントはバックフィルされませんでした。何千ものレコードが誤った型を持ったまま残り、$push がそれに当たるまで誰も気づきません。
  • 集計パイプラインアップデートが最もクリーンな一括修正方法です。 [{ $set: { field: ["$field"] } }] というパターンで、アプリケーション側にドキュメントを取り込むことなく、一度の書き込みでスカラー値を配列にラップできます。
  • スキーマバリデーションはすぐに元が取れます。 MongoDBはデフォルトで何でも書き込みます。collMod のバリデータールールを1つ追加するだけで状況が変わります。型ミスが数週間後に分かりにくいランタイムエラーとして表面化するのではなく、書き込み時点で捕捉されます。

Related Error Notes