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.
$pushand$addToSetrequire 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
$pushhits 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
collModvalidator rule changes that: type mistakes get caught at write time instead of surfacing as confusing runtime errors weeks later.

