{"record":{"id":"68462c4f26e43cd6","repo":"Automattic/mongoose","slug":"reference-virtuals-require-foreignfield-option","errorCode":null,"errorMessage":"Reference virtuals require `foreignField` option","messagePattern":"Reference virtuals require `foreignField` option","errorType":"exception","errorClass":"MongooseError","httpStatus":null,"severity":"error","filePath":"lib/schema.js","lineNumber":2597,"sourceCode":" * @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.\n * @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.\n * @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.\n * @return {VirtualType}\n */\n\nSchema.prototype.virtual = function(name, options) {\n  if (name instanceof VirtualType || getConstructorName(name) === 'VirtualType') {\n    return this.virtual(name.path, name.options);\n  }\n  options = new VirtualOptions(options);\n\n  if (utils.hasUserDefinedProperty(options, ['ref', 'refPath'])) {\n    if (options.localField == null) {\n      throw new MongooseError('Reference virtuals require `localField` option');\n    }\n\n    if (options.foreignField == null) {\n      throw new MongooseError('Reference virtuals require `foreignField` option');\n    }\n\n    const virtual = this.virtual(name);\n    virtual.options = options;\n\n    this.pre('init', function virtualPreInit(obj, opts) {\n      if (mpath.has(name, obj)) {\n        const _v = mpath.get(name, obj);\n        if (!this.$$populatedVirtuals) {\n          this.$$populatedVirtuals = {};\n        }\n\n        if (options.justOne || options.count) {\n          this.$$populatedVirtuals[name] = Array.isArray(_v) ?\n            _v[0] :\n            _v;\n        } else {\n          this.$$populatedVirtuals[name] = Array.isArray(_v) ?","sourceCodeStart":2579,"sourceCodeEnd":2615,"githubUrl":"https://github.com/Automattic/mongoose/blob/49cdab01366679723b487ecb754b38570f783289/lib/schema.js#L2579-L2615","documentation":"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.","triggerScenarios":"`schema.virtual('author', { ref: 'User', localField: 'authorId' })` with no foreignField (the author assumed _id); an option typo like `foreignfield`; a refPath variant missing foreignField.","commonSituations":"Porting manual populate() calls (where foreignField does default to _id) to virtuals and forgetting the explicit option; partially copied virtual configs.","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."],"exampleFix":"// before\nschema.virtual('author', { ref: 'User', localField: 'authorId' });\n\n// after\nschema.virtual('author', {\n  ref: 'User',\n  localField: 'authorId',\n  foreignField: '_id',\n  justOne: true\n});","handlingStrategy":"validation","validationCode":"const requiredPopulateKeys = ['localField', 'foreignField'];\nconst assertRefVirtual = (opts) => {\n  if (!('ref' in opts || 'refPath' in opts)) return;\n  for (const key of requiredPopulateKeys) {\n    if (opts[key] == null) throw new Error(`populate virtual missing ${key}`);\n  }\n};","typeGuard":"const isCompletePopulateVirtual = (o) =>\n  (o.ref != null || o.refPath != null) &&\n  typeof o.localField === 'string' &&\n  typeof o.foreignField === 'string';","tryCatchPattern":null,"preventionTips":["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."],"tags":["virtuals","populate","schema-definition","missing-option"],"backgroundTag":"virtual-populate-missing-field","analyzedSha":"49cdab01366679723b487ecb754b38570f783289","analyzedAt":"2026-08-21T22:54:00.882Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}