{"record":{"id":"fc1a80314066f5fb","repo":"Automattic/mongoose","slug":"reference-virtuals-require-localfield-option","errorCode":null,"errorMessage":"Reference virtuals require `localField` option","messagePattern":"Reference virtuals require `localField` option","errorType":"exception","errorClass":"MongooseError","httpStatus":null,"severity":"error","filePath":"lib/schema.js","lineNumber":2593,"sourceCode":" * @param {string|Function} [options.localField] Required for populate virtuals. See [populate virtual docs](https://mongoosejs.com/docs/populate.html#populate-virtuals) for more information.\n * @param {string|Function} [options.foreignField] Required for populate virtuals. See [populate virtual docs](https://mongoosejs.com/docs/populate.html#populate-virtuals) for more information.\n * @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.\n * @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()`.\n * @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) ?","sourceCodeStart":2575,"sourceCodeEnd":2611,"githubUrl":"https://github.com/Automattic/mongoose/blob/49cdab01366679723b487ecb754b38570f783289/lib/schema.js#L2575-L2611","documentation":"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.","triggerScenarios":"`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.","commonSituations":"Tutorial code trimmed for brevity; renaming source fields without updating virtual options; case-sensitive option keys typed from memory.","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."],"exampleFix":"// before\nschema.virtual('members', { ref: 'Team' });\n\n// after\nschema.virtual('members', {\n  ref: 'Team',\n  localField: 'teamId',\n  foreignField: '_id',\n  justOne: false\n});","handlingStrategy":"validation","validationCode":"const assertPopulateVirtualOptions = (opts) => {\n  const hasRef = opts != null && ('ref' in opts || 'refPath' in opts);\n  if (hasRef && opts.localField == null) throw new Error('populate virtual: localField is required');\n  if (hasRef && opts.foreignField == null) throw new Error('populate virtual: foreignField is required');\n};\nassertPopulateVirtualOptions(myOpts);\nschema.virtual('members', myOpts);","typeGuard":"const isCompletePopulateVirtual = (o) =>\n  o != null &&\n  (o.ref != null || o.refPath != null) &&\n  typeof o.localField === 'string' &&\n  typeof o.foreignField === 'string';","tryCatchPattern":null,"preventionTips":["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."],"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"}