payloadcms/payload · error · InvalidConfiguration

Virtual field in ` : `collection `} references 2 or more…

Error message

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.

What it means

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.

Solutions

  1. Reduce the virtual path so it crosses at most one hasMany relationship.
  2. Restructure so the intermediate hop is a single (belongsTo) relationship or a group/array.
  3. Compute the deeper aggregation in a hook or external query instead of a virtual.

Example fix

// before (both posts and comments are hasMany)
{ name: 'allComments', type: 'text', virtual: 'posts.comments.body' }
// after (single hasMany hop)
{ name: 'postTitles', type: 'text', virtual: 'posts.title' }
Defensive patterns

Strategy: validation

Validate before calling

// approximate static check: count hasMany relationship hops in a virtual path
function countHasManyHops(virtualPath, flatFieldsBySegment) {
  // walk segments, increment when the resolved field is a hasMany relationship/upload
  // reject if hasMany count > 1
  // (requires resolving relationTo->collection fields, similar to the sanitizer)
}

Type guard

function virtualPathHasAtMostOneHasMany(segments, resolve): boolean {
  let hasMany = 0
  for (const seg of segments) {
    const f = resolve(seg)
    if (!f) return false
    if ((f.type === 'relationship' || f.type === 'upload') && f.hasMany) hasMany++
  }
  return hasMany <= 1
}

Try / catch

try {
  await payload.init({ config })
} catch (err) {
  if (err?.name === 'InvalidConfiguration' && /hasMany relationships/i.test(err?.message ?? '')) {
    console.error('Virtual field crosses 2+ hasMany:', err?.message)
  }
  throw err
}

Prevention

When it happens

Trigger: 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.

Common situations: Modeling computed/derived fields that reach across nested collections; trying to flatten deep hierarchies into a virtual.

Related errors


AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12). Data as JSON: /api/errors/1ced73df7b9db0bd. Report an issue: GitHub.

Appendix: source

Thrown at packages/payload/src/fields/config/sanitize.ts:627

        if (
          foundField.type === 'group' ||
          foundField.type === 'tab' ||
          foundField.type === 'array'
        ) {
          flattenFields = foundField.flattenedFields
        } else if (
          (foundField.type === 'relationship' || foundField.type === 'upload') &&
          idx !== paths.length - 1 &&
          typeof foundField.relationTo === 'string'
        ) {
          if (
            foundField.hasMany &&
            (virtualField.type === 'text' ||
              virtualField.type === 'number' ||
              virtualField.type === 'select')
          ) {
            if (isHasMany) {
              throw new InvalidConfiguration(
                `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.`,
              )
            }

            isHasMany = true
            virtualField.hasMany = true
          }
          const relatedCollection = config.collections?.find(
            (e) => e.slug === foundField.relationTo,
          )
          if (relatedCollection) {
            flattenFields = flattenAllFields({ fields: relatedCollection.fields })
          }
        }
      }
    }
  }

View on GitHub (pinned to 00c58b35c0)