payloadcms/payload · error · APIError

Join fields cannot be added to arrays, blocks or globals.

Error message

Join fields cannot be added to arrays, blocks or globals.

What it means

A `join` field was placed where joins are not allowed: inside an array, inside a block, or on a global. The sanitizer detects this because the `joins` argument is `undefined` (it is only passed for top-level collection fields). Join fields need a single owning collection with a DB table, which arrays/blocks/globals cannot provide.

Source

Thrown at packages/payload/src/fields/config/sanitizeJoinField.ts:45

  joins,
  orderableJoins,
  parentIsLocalized,
  polymorphicJoins,
  validateOnly,
}: {
  config: Config
  field: FlattenedJoinField | JoinField
  joinPath?: string
  joins?: SanitizedJoins
  /** Tracker for orderable join fields - populated during sanitization */
  orderableJoins?: OrderableJoinInfo[]
  parentIsLocalized: boolean
  polymorphicJoins?: SanitizedJoin[]
  validateOnly?: boolean
}) => {
  // the `joins` arg is not passed for globals or when recursing on fields that do not allow a join field
  if (typeof joins === 'undefined') {
    throw new APIError('Join fields cannot be added to arrays, blocks or globals.')
  }
  if (!field.admin) {
    field.admin = {}
  }
  if (typeof field.maxDepth === 'undefined') {
    field.maxDepth = 1
  }
  const join: SanitizedJoin = {
    field,
    joinPath: `${joinPath ? joinPath + '.' : ''}${field.name}`,
    parentIsLocalized,
    // @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
    targetField: undefined,
  }

  // Orderable joins must target a single collection
  if (field.orderable && Array.isArray(field.collection)) {
    throw new APIError('Orderable joins must target a single collection')

View on GitHub (pinned to 00c58b35c0)

Solutions

  1. Move the join field to the top level of a collection (not nested in array/block/group/tab-within-array).
  2. If you need related data inside a nested structure, use a `relationship` field instead of a `join`.
  3. For globals, model the reverse data as a relationship on a collection instead of a join on the global.

Example fix

// before (join inside array)
{ name: 'items', type: 'array', fields: [
  { name: 'linked', type: 'join', collection: 'posts', on: 'owner' }
]}
// after (join on the collection top level)
fields: [
  { name: 'items', type: 'array', fields: [...] },
  { name: 'linkedPosts', type: 'join', collection: 'posts', on: 'owner' }
]
Defensive patterns

Strategy: validation

Validate before calling

function findJoinsInDisallowedScopes(fields, inNested = false) {
  const bad = []
  for (const f of fields) {
    if (f?.type === 'join' && inNested) bad.push(f.name)
    if (Array.isArray(f?.fields)) bad.push(...findJoinsInDisallowedScopes(f.fields, true))
    if (Array.isArray(f?.blocks)) for (const b of f.blocks) if (b?.fields) bad.push(...findJoinsInDisallowedScopes(b.fields, true))
  }
  return bad
}
// for globals, ALL joins are disallowed: scan global.fields with inNested=false but treat global context as disallowed

Type guard

function joinIsAtCollectionTopLevel(field, context): boolean {
  return field?.type !== 'join' || (context === 'collection' && !context.nested)
}

Try / catch

try {
  await payload.init({ config })
} catch (err) {
  if (err?.name === 'APIError' && /Join fields cannot be added/i.test(err?.message ?? '')) {
    console.error('join field placed in array/block/global')
  }
  throw err
}

Prevention

When it happens

Trigger: A `join` field listed in the `fields` of an `array`, in a block's `fields`, or in a global's `fields`.

Common situations: Trying to model bi-directional relationships from inside a nested structure; assuming joins work like relationships everywhere; refactoring a top-level join into a group/array.

Related errors


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