Automattic/mongoose · error · MongooseError

Reference virtuals require `foreignField` option

Error message

Reference virtuals require `foreignField` option

What it means

For a populate virtual (ref/refPath set), mongoose must know which field to match on the referenced collection; `foreignField` is that setting. The virtual() call is rejected at definition time when foreignField is missing, even if localField is present - mongoose does not default it to _id for virtuals.

Source

Thrown at lib/schema.js:2597

 * @param {Function|null} [options.get=null] Adds a [getter](https://mongoosejs.com/docs/tutorials/getters-setters.html) to this virtual to transform the populated doc.
 * @param {object|Function} [options.match=null] Apply a default [`match` option to populate](https://mongoosejs.com/docs/populate.html#match), adding an additional filter to the populate query.
 * @param {boolean} [options.applyToArray=false] If true and the given `name` is a direct child of an array, apply the virtual to the array rather than the elements.
 * @return {VirtualType}
 */

Schema.prototype.virtual = function(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'])) {
    if (options.localField == null) {
      throw new MongooseError('Reference virtuals require `localField` option');
    }

    if (options.foreignField == null) {
      throw new MongooseError('Reference virtuals require `foreignField` option');
    }

    const virtual = this.virtual(name);
    virtual.options = options;

    this.pre('init', function virtualPreInit(obj, opts) {
      if (mpath.has(name, obj)) {
        const _v = mpath.get(name, obj);
        if (!this.$$populatedVirtuals) {
          this.$$populatedVirtuals = {};
        }

        if (options.justOne || options.count) {
          this.$$populatedVirtuals[name] = Array.isArray(_v) ?
            _v[0] :
            _v;
        } else {
          this.$$populatedVirtuals[name] = Array.isArray(_v) ?

View on GitHub (pinned to 49cdab0136)

Solutions

  1. Add foreignField explicitly - usually '_id': { ref: 'User', localField: 'authorId', foreignField: '_id' }.
  2. When matching a non-_id remote field, name it exactly as declared on the remote schema.
  3. Validate the whole option triple (ref/refPath + localField + foreignField) to also avoid the companion localField error.

Example fix

// before
schema.virtual('author', { ref: 'User', localField: 'authorId' });

// after
schema.virtual('author', {
  ref: 'User',
  localField: 'authorId',
  foreignField: '_id',
  justOne: true
});
Defensive patterns

Strategy: validation

Validate before calling

const requiredPopulateKeys = ['localField', 'foreignField'];
const assertRefVirtual = (opts) => {
  if (!('ref' in opts || 'refPath' in opts)) return;
  for (const key of requiredPopulateKeys) {
    if (opts[key] == null) throw new Error(`populate virtual missing ${key}`);
  }
};

Type guard

const isCompletePopulateVirtual = (o) =>
  (o.ref != null || o.refPath != null) &&
  typeof o.localField === 'string' &&
  typeof o.foreignField === 'string';

Prevention

When it happens

Trigger: `schema.virtual('author', { ref: 'User', localField: 'authorId' })` with no foreignField (the author assumed _id); an option typo like `foreignfield`; a refPath variant missing foreignField.

Common situations: Porting manual populate() calls (where foreignField does default to _id) to virtuals and forgetting the explicit option; partially copied virtual configs.

Related errors


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