Skip to content

Inability to Migrate Primitive String Field to Subdocument Schema via get or init Hook #15389

@timheerwagen

Description

@timheerwagen

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the bug has not already been reported

Mongoose version

8.14.0

Node.js version

22.13.1

MongoDB server version

"mongodb-memory-server": "^10.1.4"

Typescript version (if applicable)

5.8.2

Description

I'm attempting to migrate a time field stored as a primitive string (e.g., "12:34") to a subdocument with hours and minutes keys. This transformation needs to apply to multiple fields in a dynamic, reusable way. So there cant be a getter or init hook on the main document.

I tried two approaches:

Using a get function on the schema field.

Adding a pre("init") hook within the subdocument schema.

However, neither strategy correctly transforms the old string value. It seems the validator strips the string before the getter or hook is executed.

Steps to Reproduce

  1. Create and save a model with a string time field.
const oldUserSchema = new Schema<{ unknownKey: string }>(
  {
    unknownKey: {
      type: String,
      required: true,
    },
  },
);

const UserModelName = "User";

const OldUser = mongoose.model(UserModelName, oldUserSchema);

await OldUser.create({ unknownKey: "12:34" });
  1. Delete old model
mongoose.deleteModel(UserModelName);
  1. Redefine it using a subdocument timeSchema and attempt to transform the string value using a getter or pre("init") hook.
type Time = { hours: number; minutes: number };

const timeStringToObject = (time: string | Time): Time => {
  if (typeof time !== "string") return time;

  const [hours, minutes] = time.split(":");

  return { hours: parseInt(hours), minutes: parseInt(minutes) };
};

const timeSchema = new Schema(
  {
    hours: { type: Number, required: true },
    minutes: { type: Number, required: true },
  },
);

const userSchema = new Schema<{ unknownKey: Time }>({
  unknownKey: {
    type: timeSchema,
    get: timeStringToObject, // try getter
    required: true,
  },
});

// try also pre init hook
timeSchema.pre("init", function (rawDoc) {
  console.log(rawDoc);
  if (typeof rawDoc === "string") {
    rawDoc = timeStringToObject(rawDoc);
  }
});

const User = mongoose.model(UserModelName, userSchema);
  1. Query documents and inspect their output
await User.create({ unknownKey: { hours: 12, minutes: 35 } });

console.log(
  await User.find(),
  (await User.find()).map((doc) => doc.toObject({ getters: true }))
);
// [
//   {
//     _id: new ObjectId('6813958e24d105ccd4d1dac7'),
//     __v: 0,
//     id: '6813958e24d105ccd4d1dac7'
//   },
//   {
//     _id: new ObjectId('6813958e24d105ccd4d1dac9'),
//     unknownKey: { hours: 12, minutes: 35 },
//     __v: 0,
//     id: '6813958e24d105ccd4d1dac9'
//   }
// ]

The string value is stripped by the validators before it can be picked up by getter or init hook.

Expected Behavior

I expected previously stored string values like "12:34" to be converted into { hours: 12, minutes: 34 } and not be stripped by validations.

Expected output:

// [
//   {
//     _id: new ObjectId('6813958e24d105ccd4d1dac7'),
//     unknownKey: { hours: 12, minutes: 34 },
//     __v: 0,
//     id: '6813958e24d105ccd4d1dac7'
//   },
//   {
//     _id: new ObjectId('6813958e24d105ccd4d1dac9'),
//     unknownKey: { hours: 12, minutes: 35 },
//     __v: 0,
//     id: '6813958e24d105ccd4d1dac9'
//   }
// ]

Metadata

Metadata

Assignees

No one assigned

    Labels

    discussionIf you have any thoughts or comments on this issue, please share them!enhancementThis issue is a user-facing general improvement that doesn't fix a bug or add a new feature

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions