payloadcms / payloadcms/payload

`restoreVersion` fails on fields combining `required: true` with `access.update: () => false` after #16272

Open
#16,718 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area: core stale status: needs-triage v3
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:

  1. Boots Payload against an in-memory SQLite DB.

  2. Creates a post with a category field defined as:

    {
      name: "category",
      type: "text",
      required: true,
      access: { update: () => false },
    }
    
  3. Updates the post (producing a second version).

  4. Calls payload.restoreVersion({ ..., overrideAccess: false }) — same flag the REST endpoint POST /api/<collection>/versions/:id uses 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.