---
title: "Migration Guide — 3.0 → 4.0"
aliases: [payload-v4-migration, payload-3-to-4]
tags: [payloadcms, migration, breaking-changes, codemod]
sources: [raw/migration-guide__v4.md]
created: 2026-05-15
updated: 2026-05-15
---

# Migration Guide — Payload 3.0 → 4.0

Use the codemod first — it handles most breaking changes automatically:

```bash
npx @payloadcms/codemod
```

The codemod is idempotent and safe on partially-migrated projects. Individual transforms can be run with `--transform <name>`.

---

## Breaking Changes

### List View Select API — now default

`admin.enableListViewSelectAPI` has been **removed**. The List View always uses the Select API (query only active columns). Remove the property if you had it set to `true`.

Codemod: `migrate-list-view-select-api`

---

### Globals: `admin.components.elements` → `admin.components.edit`

Global configs now use the same `admin.components` shape as Collections.

```diff
admin: {
  components: {
-   elements: {
-     SaveButton: '/path/to/CustomSaveButton',
-     Description: '/path/to/CustomDescription',
-   },
+   Description: '/path/to/CustomDescription',
+   edit: {
+     SaveButton: '/path/to/CustomSaveButton',
+   },
  },
}
```

Slots under `admin.components.edit` for Globals now match Collections: `beforeDocumentControls`, `editMenuItems`, `PreviewButton`, `PublishButton`, `SaveButton`, `SaveDraftButton`, `Status`, `UnpublishButton`. (`Upload` stays Collection-only.)

Globals also now support custom views via `admin.components.views` with arbitrary keys.

Codemod: `globals-components-edit`

---

### `forceSelect` → `select` function

Static `forceSelect` is removed. Each Collection/Global now accepts a top-level `select` function:

```diff
- forceSelect: {
-   title: true,
-   slug: true,
- },
+ select: ({ select }) => (select ? { ...select, title: true, slug: true } : undefined),
```

- Returning `undefined` leaves the caller's `select` unchanged.
- The function **replaces** (not deep-merges) the caller's select — spread `select` manually to preserve previous deep-merge behavior.
- Codemod handles nested `forceSelect` by importing `deepMergeSimple` from `payload/shared`.

Codemod: `migrate-force-select`

---

### `admin.hideAPIURL` removed

Use `admin.components.views.edit.api.tab.condition` instead:

```diff
- hideAPIURL: true,
+ components: {
+   views: {
+     edit: {
+       api: { tab: { condition: () => false } },
+     },
+   },
+ },
```

Codemod: `remove-hide-api-url`

---

### Aliased re-exports removed from `@payloadcms/ui` and `@payloadcms/next`

Import directly from `payload` or `payload/shared`:

| Symbol | Old source | New source |
|--------|------------|------------|
| `Column`, `ListViewSlots`, `ListViewClientProps` | `@payloadcms/ui` | `payload` |
| `EntityType`, `formatAdminURL`, `mergeListSearchAndWhere` | `@payloadcms/ui/shared` | `payload/shared` |
| `mergeHeaders`, `headersWithCors`, `createPayloadRequest`, `addDataAndFileToRequest`, `sanitizeLocales`, `addLocalesToRequestFromData` | `@payloadcms/next/utilities` | `payload` |

**Renamed types** (import from `payload`):

| Old | New |
|-----|-----|
| `ListPreferences` | `CollectionPreferences` |
| `ListComponentClientProps` | `ListViewClientProps` |
| `ListComponentServerProps` | `ListViewServerProps` |

Codemod: `migrate-aliased-exports` (adds `as` aliases so existing code compiles — rename usages manually to fully commit).

---

### `useDocumentInfo` no longer provides `title` / `setDocumentTitle`

Document title is split into its own `DocumentTitleContext`. Use `useDocumentTitle` hook:

```diff
- import { useDocumentInfo } from '@payloadcms/ui'
+ import { useDocumentTitle } from '@payloadcms/ui'

- const { title, setDocumentTitle } = useDocumentInfo()
+ const { title, setDocumentTitle } = useDocumentTitle()
```

If you also use other `useDocumentInfo` properties, keep both calls.

Codemod: `migrate-document-title-context` (handles mixed destructures, removes unused import, preserves aliases).

---

### Plugin Import/Export: `toCSV` / `fromCSV` → hooks

`toCSV` and `fromCSV` in `custom['plugin-import-export']` are removed. Use `hooks.beforeExport` / `hooks.beforeImport`:

```diff
custom: {
  'plugin-import-export': {
-   toCSV: ({ value, columnName, row, doc }) => { ... },
-   fromCSV: ({ value, data }) => ...,
+   hooks: {
+     beforeExport: ({ value, columnName, siblingData, data }) => { ... },
+     beforeImport: ({ value, data }) => ...,
+   },
  },
}
```

Arg mapping: `row` → `siblingData`, `doc` → `data`. New `format` param (`'csv' | 'json'`) available in `beforeExport`.

Codemod: `migrate-import-export-hooks` (rename `row`/`doc` references manually after migration).

---

## Key Takeaways

- **Run `npx @payloadcms/codemod` first** — covers ~80% of changes automatically; idempotent and safe to re-run.
- **`forceSelect` is gone** — new `select` function replaces (not merges); spread the caller's `select` to preserve old behavior.
- **Globals admin.components shape unified** — `elements` → `edit`, `Description` hoisted to top level.
- **Title split from DocumentInfoContext** — components that only needed `title` can drop `useDocumentInfo` entirely.
- **Re-exports cleaned up** — import `Column`, `mergeHeaders`, etc. from `payload` / `payload/shared` directly.
- **Import/Export plugin API** — `toCSV`/`fromCSV` replaced with `hooks.beforeExport`/`hooks.beforeImport`; mind renamed args (`row` → `siblingData`, `doc` → `data`).

---

## Related

- [[wiki/payloadcms/migration-guide-v2-to-v3|Migration Guide — 2.0 → 3.0]]
- [[wiki/payloadcms/migration-guide-overview|Migration Guides — Overview]]
- [[wiki/payloadcms/collection-config|Collection Config]] — `select` function, admin components
- [[wiki/payloadcms/globals-config|Globals Config]] — updated `admin.components` shape
- [[wiki/payloadcms/plugins|Official Plugins]] — Import/Export plugin hooks

---

## Sources

- `raw/migration-guide__v4.md`
- https://payloadcms.com/docs/migration-guide/v4
