Modifying immutable field _id error

I’m encountering an error that surprises me, hoping someone can share some context.
Our application uses NestJS (“@nestjs/core”: “^11.1.0”,) and mongoose (“mongoose”: “^8.5.3”).

What I’m trying to accomplish is upserting users from an earlier version of our application (different mongo db) to our current version, I need to preserve the _id field from the legacy document when creating the new one.

This is my query to do so:

const updatedUser = await this.userModel
          .findOneAndUpdate(
            {
              $or: [
                { _id: dto._id },
                { ...(dto.phone ? { phone: dto.phone } : {}) },
                { ...(dto.email ? { email: dto.email } : {}) },
                { ...(dto.orderingCode ? { reuserId: dto.orderingCode } : {}) },
              ],
            },
            newUserDocument,
            {
              upsert: true,
              new: true,
            },
          )

where newUserDocument contains the minimum necessary fields for creating a user based on the ones provided in the dto. However, the dto also includes an _id field from the V1 DB, so that my new documents retain the same _id as the legacy documents. It is a requirement that the _id be the same as the original DB, as our users save a QR of their account info for offline use, and, unfortunately it uses that _id field as the key.

The email, phone, and orderingCode are values that might be used to de-duplicate users.

The error blocking me from moving forward:

Plan executor error during findAndModify :: caused by :: Performing an update on the path '_id' would modify the immutable field '_id'

I notice that this error arises conditionally based on the fields provided in the dto. If I send an object with email, orderingCode and phone, the upsert proceeds and the returned document has the same _id as the dto.

If the dto does not have any of those fields, the query fails with the above error. For ref, here is an example working dto:

{
  "_id":"6749f9ac29adbf976a058303",
  "email":"test@gmail.com",
  "tag":"webapp",
  "phone":"+15555557053",
  "orderingCode":"3384",
  "customer":"cus_123123",
  "hasCard":true,
  "fullName":"Full Name"
}

And if I run the same query again I am returned the same document as expected.

If I use this dto (without a phone field), it fails with the provided error:

{
  "_id":"6749f9ac29adbf976a058303",
  "email":"test@gmail.com",
  "tag":"webapp",
  "orderingCode":"3384",
  "customer":"cus_123123",
  "hasCard":true,
  "fullName":"Full Name"
}

Any ideas why this would be? To clarify, the intent is to upsert users based on any of their existing _id, email, phone or orderingCode. orderingCode is a marked as unique via mongoose, email is a unique sparse index and phone is unique and sparse. _id is of type mongoose.Schema.Types.ObjectId

Hi, welcome to MongoDB Community :smiley:

If I understood correctly:

The issue occurs because the _id field in MongoDB is immutable. When you run findOneAndUpdate and the document already exists, MongoDB tries to update the _id, causing the error:

Performing an update on the path _id would modify the immutable field _id

Why It Works Sometimes:

  • If the document does not exist, findOneAndUpdate with upsert: true creates it with the provided _id.
  • If the document already exists, it tries to update the _id, which is not allowed.

To solve:

  1. Find the document first .
  2. If it exists, update its fields except _id.
  3. If not, create a new document with the specified _id.

Refactored Code:

const existingUser = await this.userModel.findOne({
  $or: [
    { _id: dto._id },
    { phone: dto.phone },
    { email: dto.email },
    { reuserId: dto.orderingCode },
  ].filter(Boolean),
});

if (existingUser) {
  Object.assign(existingUser, newUserDocument);
  await existingUser.save();
  return existingUser;
} else {
  const createdUser = new this.userModel(newUserDocument);
  await createdUser.save();
  return createdUser;
}

Does this help?

Thanks for taking the time to reply, but no, I understand immutable _id errors, but the issue doesn’t follow that pattern. When the document already exists (When there is a match on _id), I am returned that document and no upsert takes place, as expected. The issue arises when one of the other fields to de-duplicate (like phone) is omitted.