Automattic/mongoose · error · MongooseError

Cannot set populate virtual as a property of an array

Error message

Cannot set populate virtual as a property of an array

What it means

Array-level virtuals (attached to the MongooseArray itself via SchemaArray#virtual) support plain computed getters/setters only. ref/refPath populate virtuals need a document field to join from, which an array cannot provide, so defining one on an array is rejected.

Solutions

  1. Define the populate virtual on the embedded subdocument's schema (each element populates its own owner), or on the parent document with localField/foreignField that fan out correctly.
  2. Keep ref/refPath out of array-level virtuals - use those for computed values only.

Example fix

// before
const itemSchema = new Schema({ ownerId: ObjectId });
const s = new Schema({ items: [itemSchema] });
s.path('items').virtual('owner', { ref: 'User' }); // throws

// after
itemSchema.virtual('owner', { ref: 'User', localField: 'ownerId', foreignField: '_id', justOne: true });
// then: await doc.populate('items.owner');
Defensive patterns

Strategy: validation

Validate before calling

const assertNoPopulateOnArrayVirtual = (opts) => {
  if (opts != null && ('ref' in opts || 'refPath' in opts)) {
    throw new Error('populate virtuals must live on a document schema, not on an array');
  }
};
schema.path('items').virtual('firstUpper');

Prevention

When it happens

Trigger: `schema.path('items').virtual('owner', { ref: 'User' })`; a schema.virtual call with applyToArray whose options include ref/refPath, landing on the array type.

Common situations: Trying to populate 'the owner of every element' from the parent array; migrating a document-level populate virtual onto an array during schema reshaping.

Related errors


AI-assisted analysis of Automattic/mongoose@49cdab0136 (2026-08-21). Data as JSON: /api/errors/22cd1a8cee5d16da. Report an issue: GitHub.

Appendix: source

Thrown at lib/schema/array.js:568

  }
};

/**
 * Add a virtual to this array. Specifically to this array, not the individual elements.
 *
 * @param {string} name
 * @param {object} [options]
 * @api private
 */

SchemaArray.prototype.virtual = function virtual(name, options) {
  if (name instanceof VirtualType || getConstructorName(name) === 'VirtualType') {
    return this.virtual(name.path, name.options);
  }
  options = new VirtualOptions(options);

  if (utils.hasUserDefinedProperty(options, ['ref', 'refPath'])) {
    throw new MongooseError('Cannot set populate virtual as a property of an array');
  }

  const virtual = new VirtualType(options, name);
  if (this.virtuals === null) {
    this.virtuals = {};
  }
  this.virtuals[name] = virtual;
  return virtual;
};

function cast$all(val, context) {
  if (!Array.isArray(val)) {
    val = [val];
  }

  val = val.map((v) => {
    if (!utils.isObject(v)) {
      return v;

View on GitHub (pinned to 49cdab0136)