payloadcms/payload · error · InvalidConfiguration
Virtual field ${virtualField.name} in ${globalConfig ? `glob
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.
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)
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.
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
- 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.
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
- Field "${field.name}" of type "${field.type}" has an empty r
- Invalid join field ${field.name}. The config does not have a
- Field "${field.name}" is either missing a field type or it d
- Field ${field.label} has reserved name '${fieldName}'.
- Field ${field.label} has invalid name '${fieldName}'. Field
AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12).
Data as JSON: /api/errors/1ced73df7b9db0bd.
Report an issue: GitHub.