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
- Add localField naming a field on the owning document: { ref: 'Team', localField: 'teamId', foreignField: '_id' }.
- Add foreignField too - mongoose requires both for populate virtuals (see the companion foreignField error).
- 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
- Centralize populate-virtual definitions behind a helper that validates the option triple (ref/refPath + localField + foreignField).
- Add a unit test that walks schema.virtuals and asserts every ref virtual has both fields.
- Copy populate configs whole - trimming one option breaks the join.
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
- Reference virtuals require `foreignField` option
- Cannot set populate virtual as a property of an array
- Virtual path "${name}" conflicts with a real path in the sch
- Path "${path}" is not an array
- Invalid addFields() argument. Must be an object
AI-assisted analysis of Automattic/mongoose@49cdab0136 (2026-08-21).
Data as JSON: /api/errors/fc1a80314066f5fb.
Report an issue: GitHub.