Automattic/mongoose · error · MongooseError

Reference virtuals require `localField` option

Error message

Reference virtuals require `localField` option

What it means

A virtual that declares `ref` or `refPath` becomes a populate virtual: mongoose must issue a query against the referenced collection, and `localField` (which field on the owning document to match with) is mandatory. Without it the join is ambiguous, so Schema#virtual() rejects the definition at schema-build time.

Source

Thrown at lib/schema.js:2593

 * @param {string|Function} [options.localField] Required for populate virtuals. See [populate virtual docs](https://mongoosejs.com/docs/populate.html#populate-virtuals) for more information.
 * @param {string|Function} [options.foreignField] Required for populate virtuals. See [populate virtual docs](https://mongoosejs.com/docs/populate.html#populate-virtuals) for more information.
 * @param {boolean|Function} [options.justOne=false] Only works with populate virtuals. If [truthy](https://masteringjs.io/tutorials/fundamentals/truthy), will be a single doc or `null`. Otherwise, the populate virtual will be an array.
 * @param {boolean} [options.count=false] Only works with populate virtuals. If [truthy](https://masteringjs.io/tutorials/fundamentals/truthy), this populate virtual will contain the number of documents rather than the documents themselves when you `populate()`.
 * @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) ?

View on GitHub (pinned to 49cdab0136)

Solutions

  1. Add localField naming a field on the owning document: { ref: 'Team', localField: 'teamId', foreignField: '_id' }.
  2. Add foreignField too - mongoose requires both for populate virtuals (see the companion foreignField error).
  3. Verify exact casing/spelling of option keys: localField, foreignField, ref, refPath.

Example fix

// before
schema.virtual('members', { ref: 'Team' });

// after
schema.virtual('members', {
  ref: 'Team',
  localField: 'teamId',
  foreignField: '_id',
  justOne: false
});
Defensive patterns

Strategy: validation

Validate before calling

const assertPopulateVirtualOptions = (opts) => {
  const hasRef = opts != null && ('ref' in opts || 'refPath' in opts);
  if (hasRef && opts.localField == null) throw new Error('populate virtual: localField is required');
  if (hasRef && opts.foreignField == null) throw new Error('populate virtual: foreignField is required');
};
assertPopulateVirtualOptions(myOpts);
schema.virtual('members', myOpts);

Type guard

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

Prevention

When it happens

Trigger: `schema.virtual('members', { ref: 'Team' })` with no localField; an option typo such as `localfield` (lowercase f) leaving the real option null; copying a populate config and dropping one key.

Common situations: Tutorial code trimmed for brevity; renaming source fields without updating virtual options; case-sensitive option keys typed from memory.

Related errors


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