{"record":{"id":"1ced73df7b9db0bd","repo":"payloadcms/payload","slug":"virtual-field-virtualfield-name-in-globalconf","errorCode":null,"errorMessage":"Virtual field ${virtualField.name} in ${globalConfig ? `global ${globalConfig.slug}` : `collection ${collectionConfig?.slug}`} references 2 or more hasMany relationships on the path ${virtualField.virtual} which is not allowed.","messagePattern":"Virtual field (.+?) in (.+?)` : `collection (.+?)`\\} references 2 or more hasMany relationships on the path (.+?) which is not allowed\\.","errorType":"exception","errorClass":"InvalidConfiguration","httpStatus":500,"severity":"error","filePath":"packages/payload/src/fields/config/sanitize.ts","lineNumber":627,"sourceCode":"        if (\n          foundField.type === 'group' ||\n          foundField.type === 'tab' ||\n          foundField.type === 'array'\n        ) {\n          flattenFields = foundField.flattenedFields\n        } else if (\n          (foundField.type === 'relationship' || foundField.type === 'upload') &&\n          idx !== paths.length - 1 &&\n          typeof foundField.relationTo === 'string'\n        ) {\n          if (\n            foundField.hasMany &&\n            (virtualField.type === 'text' ||\n              virtualField.type === 'number' ||\n              virtualField.type === 'select')\n          ) {\n            if (isHasMany) {\n              throw new InvalidConfiguration(\n                `Virtual field ${virtualField.name} in ${globalConfig ? `global ${globalConfig.slug}` : `collection ${collectionConfig?.slug}`} references 2 or more hasMany relationships on the path ${virtualField.virtual} which is not allowed.`,\n              )\n            }\n\n            isHasMany = true\n            virtualField.hasMany = true\n          }\n          const relatedCollection = config.collections?.find(\n            (e) => e.slug === foundField.relationTo,\n          )\n          if (relatedCollection) {\n            flattenFields = flattenAllFields({ fields: relatedCollection.fields })\n          }\n        }\n      }\n    }\n  }\n","sourceCodeStart":609,"sourceCodeEnd":645,"githubUrl":"https://github.com/payloadcms/payload/blob/00c58b35c0ed348ddc22daabf467b139727214fd/packages/payload/src/fields/config/sanitize.ts#L609-L645","documentation":"A virtual field's `virtual` path traverses two or more `hasMany` relationship/upload hops. Payload can resolve a virtual over a single hasMany (returning an array), but chaining two hasMany relationships would produce a fan-out of fan-outs that cannot be projected into one column, so it is rejected as invalid configuration.","triggerScenarios":"A virtual field on collection A with `virtual: 'posts.comments.body'` where both `posts` (on A) and `comments` (on Post) are hasMany relationships; any path with two hasMany segments.","commonSituations":"Modeling computed/derived fields that reach across nested collections; trying to flatten deep hierarchies into a virtual.","solutions":["Reduce the virtual path so it crosses at most one hasMany relationship.","Restructure so the intermediate hop is a single (belongsTo) relationship or a group/array.","Compute the deeper aggregation in a hook or external query instead of a virtual."],"exampleFix":"// before (both posts and comments are hasMany)\n{ name: 'allComments', type: 'text', virtual: 'posts.comments.body' }\n// after (single hasMany hop)\n{ name: 'postTitles', type: 'text', virtual: 'posts.title' }","handlingStrategy":"validation","validationCode":"// approximate static check: count hasMany relationship hops in a virtual path\nfunction countHasManyHops(virtualPath, flatFieldsBySegment) {\n  // walk segments, increment when the resolved field is a hasMany relationship/upload\n  // reject if hasMany count > 1\n  // (requires resolving relationTo->collection fields, similar to the sanitizer)\n}","typeGuard":"function virtualPathHasAtMostOneHasMany(segments, resolve): boolean {\n  let hasMany = 0\n  for (const seg of segments) {\n    const f = resolve(seg)\n    if (!f) return false\n    if ((f.type === 'relationship' || f.type === 'upload') && f.hasMany) hasMany++\n  }\n  return hasMany <= 1\n}","tryCatchPattern":"try {\n  await payload.init({ config })\n} catch (err) {\n  if (err?.name === 'InvalidConfiguration' && /hasMany relationships/i.test(err?.message ?? '')) {\n    console.error('Virtual field crosses 2+ hasMany:', err?.message)\n  }\n  throw err\n}","preventionTips":["Model deep derived data with hooks/queries instead of multi-hop virtuals.","When designing a virtual path, count hasMany segments; redesign if more than one."],"tags":["virtual","relationship","config","schema","sanitization"],"backgroundTag":null,"analyzedSha":"00c58b35c0ed348ddc22daabf467b139727214fd","analyzedAt":"2026-08-12T20:45:03.758Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}