Skip to content

fix: apply localized defaultValue only to the current locale#17407

Open
MurzNN wants to merge 3 commits into
payloadcms:mainfrom
MurzNN:fix/localized-default-value
Open

fix: apply localized defaultValue only to the current locale#17407
MurzNN wants to merge 3 commits into
payloadcms:mainfrom
MurzNN:fix/localized-default-value

Conversation

@MurzNN

@MurzNN MurzNN commented Jul 19, 2026

Copy link
Copy Markdown

Which branch should this PR target?

  • main — Payload v4 development

What?

Fixes a bug where localized fields with defaultValue had that default applied to all locales instead of only the locale being created or updated.

Changes span the Payload field hooks (create/update/read paths), the Drizzle adapter (schema + read transform), and the MongoDB adapter (schema). Adds a regression test suite at test/localized-default-value/.

Why?

When a field is marked localized: true and has a defaultValue, Payload is expected to apply that default only for the active locale during create/update, and not when reading locales that were never written.

The bug had multiple causes across the stack:

1. Write path — beforeValidate applied defaults on update

During update, beforeValidate still ran getFallbackValue() for localized fields whose value was undefined in the incoming payload. That meant updating locale es with { title: 'Spanish title' } could still populate myField with the default, even though the field was not in the request body.

2. Write path — beforeChange merged defaults into the active locale on update

The locale-merge logic in beforeChange treated the active locale the same on create and update. On update, if the field was omitted from the request, it could still pick up a default (or a fallback-filled value from the fetched document) for the locale being updated.

3. Read path — afterRead applied defaults to unwritten locales

When reading a document (especially with locale: 'all'), afterRead applied defaultValue to any locale key that was undefined, including locales that had never been written. That made findByID({ locale: 'es' }) return a default even when only en had been created.

4. Drizzle adapter — SQL column defaults on _locales tables (Postgres/SQLite)

For SQL adapters, localized field columns live in a separate _locales table. The schema builder used withDefault() to set SQL DEFAULT constraints from field.defaultValue on those columns.

On update, Drizzle deletes all locale rows for a document and re-inserts them. When inserting a new locale row for a partial update (e.g. only title for es), omitted columns like my_field were not included in the INSERT — so SQLite/Postgres silently applied the column default ('Default Value'), bypassing Payload hooks entirely.

5. MongoDB adapter — Mongoose defaults on locale sub-schemas

MongoDB stores localized values as nested subdocuments (myField.en, myField.es). localizeSchema copied the full Mongoose sub-schema — including default: field.defaultValue — onto every locale path. Mongoose then applied those defaults for locales that were never written, producing the same user-visible bug as the SQL column-default issue (e.g. findByID({ locale: 'all' }) returning myField.es: 'Default Value' after creating only in en).

How?

Payload hooks (packages/payload/src/fields/hooks/):

  • beforeValidate

    • Track req.context.incomingFieldKeys on update so downstream hooks know which fields were explicitly sent.
    • Skip applying defaultValue to localized fields during update (shouldApplyLocalizedDefaultValue).
    • Use req.locale || defaultLocale (not empty string) in getFallbackValue.
  • beforeChange

    • On update, when a localized field was not in the incoming data: preserve values for other locales from docWithLocales, skip the active locale, and do not apply defaults.
    • On create, continue applying defaultValue only for operationLocale.
  • afterRead

    • Capture the stored localized object before hoisting.
    • Only apply defaultValue on read when the requested locale actually exists in storage (or when reading non-localized / locale: 'all' edge cases are handled safely).
    • When locale: 'all', strip locale keys that were never stored or are null (leftover from partial locale rows in SQL adapters).

Drizzle adapter (packages/drizzle/):

  • schema/withDefault.ts — Do not set SQL column defaults on columns in _locales tables. Localized defaults must be applied in Payload hooks for the active locale only.
  • transform/read/traverseFields.ts — Skip null values when assembling localized field objects, so partial locale rows do not surface as null/default for fields that were never written in that locale.

MongoDB adapter (packages/db-mongodb/):

  • models/buildSchema.ts — Strip Mongoose default from sub-schemas before wrapping them in localizeSchema, so defaults are not baked into every locale path at the schema level.

Tests:

  • test/localized-default-value/int.spec.ts — Covers create, update, read-with-fallback-disabled, and function defaultValue with locale awareness.

Test plan

  • pnpm test:int localized-default-value (SQLite) — 4/4 passing
  • pnpm test:int localized-default-value (MongoDB) — 4/4 passing
  • Existing locale/defaultValue regression in test/fields (should update without overwriting other locales with defaultValue)

Fixes #17406

req,
}: Args<T>): Promise<T> => {
if (operation === 'update') {
req.context.incomingFieldKeys = new Set(Object.keys(incomingData))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would this work for nested fields?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Field's defaultValue applied to all locales instead of the current locale only

2 participants