payloadcms / payloadcms/payload
`restoreVersion` fails on fields combining `required: true` with `access.update: () => false` after #16272
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 44.8k
- Forks
- 4.2k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 53
Description
Describe the Bug
After PR #16272 (Payload 3.84), restoring a version of a document fails with ValidationError: <field> — This field is required for any field that combines:
required: true- field-level
access: { update: () => false }
This pattern is used to express "this field is set on create and is immutable thereafter" — it's in Payload's own examples and a common community recipe. Pre-3.84 the same pattern restored cleanly; post-3.84 it returns 400 on every restore attempt.
The change in #16272 is itself a justified fix (don't backfill restored docs from current values — agree with the goal), but it interacts badly with field-level access enforcement during restoreVersion, which is unchanged from before.
Expected Behavior
The previous version is restored. The immutable field's value comes from the version snapshot. overrideAccess: false should not strip values that the snapshot already carries.
Actual Behavior
ValidationError: The following field is invalid: Category
at beforeChange (payload/dist/fields/hooks/beforeChange/index.js:40)
at restoreVersionOperation (payload/dist/collections/operations/restoreVersion.js:179)
Root cause
In fields/hooks/beforeValidate/promise.js, field-level access is enforced for every operation including restore:
if (field.access && field.access[operation]) {
const result = overrideAccess ? true : await field.access[operation]({ ... });
if (!result) {
delete siblingData[field.name]; // category is stripped here on restore
}
}
Pre-3.84 the very next block silently rescued the value via getFallbackValue (which reads from siblingDoc). #16272 correctly skips that fallback during restore:
if (typeof siblingData[field.name] === 'undefined' && !req.context?.isRestoringVersion) {
siblingData[field.name] = await getFallbackValue(...);
}
But the access strip a few lines above still runs. So a stripped value that was present in the snapshot is now permanently lost, and required fields then fail validation in beforeChange.
In other words: the snapshot is no longer treated as authoritative for restore (which is the new, correct behavior for missing values), but field-level access is still allowed to remove values from the snapshot (which is the old behavior, and now produces broken restores).
Related
- #16272 — the change that surfaced this
- #11549 — earlier fix to a related access-strip / fallback interaction; same shape of bug, different trigger
Link to the code that reproduces this issue
https://github.com/lennert-tapart/payload-restore-version-repro
Reproduction Steps
pnpm install && pnpm repro in the linked repo. The script:
-
Boots Payload against an in-memory SQLite DB.
-
Creates a
postwith acategoryfield defined as:{ name: "category", type: "text", required: true, access: { update: () => false }, } -
Updates the post (producing a second version).
-
Calls
payload.restoreVersion({ ..., overrideAccess: false })— same flag the REST endpointPOST /api/<collection>/versions/:iduses by default.
Which area(s) are affected?
area: core
Environment Info
- Payload: 3.84.1 (also reproduces on later 3.84.x)
- Node: 22.x
- Database adapter: any (repro uses `@payloadcms/db-sqlite`; originally hit on `@payloadcms/db-postgres`)
- Last known good: 3.83.x (pre-#16272). Pre-#16272 restore worked accidentally, via the silent fallback that PR removed — this is a real interaction bug, not just a behavior change.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with fields/hooks/beforeValidate/promise.js and collections/operations/restoreVersion.js, then run the linked reproduction with pnpm install && pnpm repro. Trace restoreVersion with overrideAccess: false for a required field using access.update, and consider the work complete when the snapshot value survives and the version restores without a required-field validation error.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100