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
- Add foreignField explicitly - usually '_id': { ref: 'User', localField: 'authorId', foreignField: '_id' }.
- When matching a non-_id remote field, name it exactly as declared on the remote schema.
- 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
- Remember foreignField has no _id default on virtuals (unlike populate() calls) - always write it out.
- Validate virtual options in one shared factory so no call site can omit a field.
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
- Reference virtuals require `localField` 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/68462c4f26e43cd6.
Report an issue: GitHub.