{"record":{"id":"34e6bc4bef6d7373","repo":"Automattic/mongoose","slug":"virtual-path-name-conflicts-with-a-real-path","errorCode":null,"errorMessage":"Virtual path \"${name}\" conflicts with a real path in the schema","messagePattern":"Virtual path \"(.+?)\" conflicts with a real path in the schema","errorType":"exception","errorClass":"MongooseError","httpStatus":null,"severity":"error","filePath":"lib/schema.js","lineNumber":2693,"sourceCode":"        this.paths[cur].schema.virtual(remnant, options);\n        break;\n      } else if (this.paths[cur].$isSchemaMap) {\n        const remnant = parts.slice(i + 2).join('.');\n        this.paths[cur].$__schemaType.schema.virtual(remnant, options);\n        break;\n      }\n\n      cur += '.' + parts[i + 1];\n    }\n\n    return virtual;\n  }\n\n  const virtuals = this.virtuals;\n  const parts = name.split('.');\n\n  if (this.pathType(name) === 'real') {\n    throw new MongooseError('Virtual path \"' + name + '\"' +\n      ' conflicts with a real path in the schema');\n  }\n\n  virtuals[name] = parts.reduce(function(mem, part, i) {\n    mem[part] || (mem[part] = (i === parts.length - 1)\n      ? new VirtualType(options, name)\n      : {});\n    return mem[part];\n  }, this.tree);\n\n  if (options?.applyToArray && parts.length > 1) {\n    const path = this.path(parts.slice(0, -1).join('.'));\n    if (path?.$isMongooseArray) {\n      return path.virtual(parts[parts.length - 1], options);\n    } else {\n      throw new MongooseError(`Path \"${path}\" is not an array`);\n    }\n  }","sourceCodeStart":2675,"sourceCodeEnd":2711,"githubUrl":"https://github.com/Automattic/mongoose/blob/49cdab01366679723b487ecb754b38570f783289/lib/schema.js#L2675-L2711","documentation":"Before registering a virtual, mongoose checks pathType(name); a result of 'real' means a stored path already occupies that exact name. A virtual cannot shadow a persisted field (its getters/setters would fight the real path), so the definition is refused.","triggerScenarios":"`new Schema({ name: String })` followed by `schema.virtual('name')`; `schema.virtual('a.b')` when 'a.b' is a real nested path; plugins registering virtuals over existing fields on schemas they do not control.","commonSituations":"Adding a computed getter that collides with a stored field (e.g. a stored 'fullName' plus a computed one); plugin/schema composition across teams; renaming fields but keeping old virtuals under the old names.","solutions":["Rename the virtual to a free name (e.g. 'displayName', 'nameVirtual').","Remove or rename the stored field if the virtual replaces it.","When names are dynamic, guard with `if (schema.pathType(name) !== 'real') schema.virtual(name)`."],"exampleFix":"// before\nconst s = new Schema({ name: String });\ns.virtual('name'); // conflicts with the stored path\n\n// after\nconst s = new Schema({ name: String });\ns.virtual('displayName').get(function() { return this.name?.toUpperCase(); });","handlingStrategy":"validation","validationCode":"const safeVirtual = (schema, name, opts) => {\n  if (schema.pathType(name) === 'real') {\n    throw new Error(`path ${name} already exists as a real path; pick another virtual name`);\n  }\n  return schema.virtual(name, opts);\n};","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Namespace plugin virtuals (e.g. 'myPluginComputed') to avoid collisions with stored fields.","Grep the schema definition for a field name before adding a virtual with the same name.","When names are user-driven, check schema.pathType(name) first."],"tags":["virtuals","path-conflict","schema-definition"],"backgroundTag":"schema-path-conflict","analyzedSha":"49cdab01366679723b487ecb754b38570f783289","analyzedAt":"2026-08-21T22:54:00.882Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}