Fix MongoDB Error: 'The path must be an array' When Using $push or $addToSet

intermediate๐Ÿƒ MongoDB2026-06-03| MongoDB 4.x / 5.x / 6.x / 7.x, any OS (Linux, macOS, Windows), with Mongoose, PyMongo, or native drivers

Error Message

The path 'tags' must be an array in the document, but is of type string
#mongodb#push#addToSet#array#type-mismatch#schema-validation#mongoose#aggregation

What just happened

You ran an update with $push or $addToSet, expecting to append a value to an array โ€” but MongoDB stopped you cold:

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

The field exists in the document, but it holds a string, not an array. $push and $addToSet only work on actual arrays. Rather than silently corrupt your data, MongoDB refuses the entire operation.

Reproduce it in 30 seconds

Open mongosh and run this:

// Insert a doc where 'tags' is a plain string
db.articles.insertOne({ _id: 1, title: "Hello", tags: "mongodb" })

// Now try to push another tag
db.articles.updateOne({ _id: 1 }, { $push: { tags: "nodejs" } })
// MongoServerError: The path 'tags' must be an array...

The field is a string, not ["mongodb"]. That's the whole problem.

Step 1: Confirm the field type in the affected document

Before touching anything, look at what's actually stored:

db.articles.findOne({ _id: 1 }, { tags: 1 })
// { _id: 1, tags: "mongodb" }  โ† string, not array

Not sure how many documents are affected? Find every one where tags isn't an array:

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

Run this before you change anything. You want the full picture first.

Step 2: Fix documents that already have the wrong type

Case A โ€” Field holds a single string value

Wrap the existing string inside an array, then add the new item:

// For a single known document
const doc = db.articles.findOne({ _id: 1 })
const existingTag = doc.tags  // "mongodb"

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

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

Case B โ€” Bulk-fix all affected documents

Got hundreds of bad documents? Use an aggregation pipeline update to wrap every scalar in an array โ€” one operation, no app-side loops:

db.articles.updateMany(
  {
    tags: { $exists: true },
    $expr: { $ne: [{ $type: "$tags" }, "array"] }
  },
  [
    { $set: { tags: ["$tags"] } }  // pipeline update โ€” wraps the value in []
  ]
)

The square brackets wrapping { $set: ... } are not a typo. That's the aggregation pipeline update syntax (MongoDB 4.2+). It lets you reference the document's own field value ("$tags") on the right side of $set โ€” something a regular update expression can't do.

Case C โ€” Field is null, missing, or some other non-string type

Sometimes the field is null or simply doesn't exist at all. Reset it to an empty array first, then push:

// Set to [] for every document where tags isn't already an array
db.articles.updateMany(
  { $expr: { $ne: [{ $type: "$tags" }, "array"] } },
  { $set: { tags: [] } }
)

// Now $push works normally
db.articles.updateMany({}, { $push: { tags: "mongodb" } })

Step 3: Prevent it from happening again

Option 1 โ€” JSON Schema validation (recommended)

Add a schema rule so MongoDB rejects any write that stores a non-array value in tags:

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" rejects existing bad docs too
})

From that point on, any insert that sends tags: "mongodb" instead of tags: ["mongodb"] fails immediately with a clear validation error. Catch it at write time rather than hours later when $push starts blowing up in production.

Option 2 โ€” Mongoose schema type enforcement

If you use Mongoose, declare the field as an array explicitly in your schema:

const articleSchema = new mongoose.Schema({
  title: String,
  tags: { type: [String], default: [] }  // always an array
})

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

Mongoose coerces the type on save. You also get Mongoose document methods like doc.tags.push("nodejs") working correctly out of the box โ€” no raw MongoDB driver calls needed.

Option 3 โ€” Use $addToSet with an $each guard

Once the data is clean, prefer $addToSet over $push for tag fields. It skips values already in the array automatically:

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

Verify the fix worked

// 1. Check the specific document
db.articles.findOne({ _id: 1 }, { tags: 1 })
// Expected: { _id: 1, tags: [ "mongodb", "nodejs" ] }

// 2. Confirm no more non-array tags fields exist
db.articles.countDocuments({
  tags: { $exists: true },
  $expr: { $ne: [{ $type: "$tags" }, "array"] }
})
// Expected: 0

// 3. Run the original $push again โ€” should work now
db.articles.updateOne({ _id: 1 }, { $push: { tags: "express" } })
// Expected: { acknowledged: true, matchedCount: 1, modifiedCount: 1 }

Why this keeps happening (and how to stop it)

  • The root cause is a type mismatch. $push and $addToSet require an actual array. A single-value string like "mongodb" doesn't qualify โ€” the operation fails entirely rather than silently producing corrupted data.
  • Migrations are the usual culprit. An older version of the app stored one tag as a plain string. A schema change later introduced multi-tag support with arrays, but the old documents were never backfilled. Thousands of records end up with the wrong type, and nobody notices until a $push hits one.
  • The aggregation pipeline update is the cleanest bulk fix. The pattern [{ $set: { field: ["$field"] } }] wraps any scalar into an array in a single write โ€” no need to pull documents into your application first.
  • Schema validation pays for itself fast. MongoDB stores whatever you give it by default. One collMod validator rule changes that: type mistakes get caught at write time instead of surfacing as confusing runtime errors weeks later.

Related Error Notes