Skip to content

fix(plugin-import-export): import fields whose names contain underscores - #17515

Draft
nathanlentz wants to merge 1 commit into
mainfrom
fix/import-export-snake-case-field-names
Draft

fix(plugin-import-export): import fields whose names contain underscores#17515
nathanlentz wants to merge 1 commit into
mainfrom
fix/import-export-snake-case-field-names

Conversation

@nathanlentz

@nathanlentz nathanlentz commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Fixes #17118

Problem

unflattenObject rebuilt the nested document from a CSV row by splitting every column name on _, without consulting the field schema it already receives. Any field whose own name contains an underscore was therefore read as a nested path:

vat_number: 'IT12345678901'  ->  { vat: { number: 'IT12345678901' } }

vat is not a real field, so the collection operation stripped it and the column imported as a silent no-op — exactly the reported symptom (export, edit the CSV, re-import, field unchanged).

Export was never affected: flattenObject walks the real nested object and emits the correct vat_number column. The break is import-only, and it also affected the import preview, which shares the code path.

Other cases that followed from the same split:

  • _start_with_underscore — leading empty segment dropped
  • with_numbers_1 — trailing digit treated as an array index
  • vat_number_en — localized snake_case nested five levels deep
  • the same, one level down, inside a group

Fix

Resolve column names against the field schema instead of guessing from the delimiter. New utility flatKeyToPathSegments walks fields, matching field names longest-first with backtracking, and emits array indices, block slugs and locale codes as their own segments. Groups, named tabs, arrays, blocks and localized fields are all handled recursively, including underscores in the container's own name.

The integration in unflattenObject is one line:

const pathSegments =
  flatKeyToPathSegments({ ...segmentArgs, flatKey }) ?? flatKey.split('_')

The 190-line traversal loop below it is untouched — it just receives correct segments now.

Why this is not a breaking change

Two properties do the work:

  1. Identical segments for keys that already worked. Because indices, slugs and locales are emitted as separate segments, resolution reproduces exactly what split('_') produced. There is an explicit test asserting flatKeyToPathSegments(...) === flatKey.split('_') across locale, array, group and polymorphic keys.
  2. undefined falls back to the naive split. Anything the schema cannot account for keeps today's behaviour verbatim: extra user columns, upload sizes_thumbnail_url, and the populated relationship data an export at depth: 1 produces (author_email_address).

So the only keys whose handling changes are ones where a matched schema name — field name, block slug, or an en_US-style locale code — itself contains an underscore. Every one of those was broken before.

Ambiguity note: where a flat field and a group path collide (vat_number vs. a vat group containing number) the flat field wins. That schema is ambiguous on export too, since both produce a single vat_number column, so it cannot be resolved from the CSV alone. Backtracking still picks the group when the flat name can't consume the whole key (vat_number_suffix -> ['vat', 'number_suffix']).

Tests

  • flatKeyToPathSegments.spec.ts — 29 unit tests: flat/nested/tab/array/blocks/polymorphic resolution, the collision tie-break, backtracking, locale handling, block slug references, and the naive-split-equivalence guarantee
  • unflattenObject.spec.ts — the issue's reproduction cases, including the reporter's proposed tests
  • int.spec.ts — new posts-with-snake-case-fields collection and 4 integration tests covering the reported flow end to end: flat, group, array, per-locale, and a full export -> edit -> import round trip

With the fix reverted, exactly the 4 new integration tests fail and the other 237 pass.

`unflattenObject` rebuilt the nested document by splitting every CSV column
name on `_`, so a field whose own name contains an underscore was read as a
nested path: `vat_number` became `{ vat: { number } }`. The phantom `vat` key
is not a real field, so the collection operation stripped it and the column
imported as a no-op.

Resolve column names against the field schema instead. `flatKeyToPathSegments`
matches field names longest-first with backtracking, emitting array indices,
block slugs and locale codes as their own segments — so a key the naive split
already handled correctly resolves to the identical segments. Keys the schema
cannot account for (extra columns, the populated relationship data an export
at depth > 0 produces) return `undefined` and fall back to the naive split,
leaving their handling unchanged.

Groups, named tabs, arrays, blocks and localized fields are all resolved
recursively, including underscores in the container's own name. Where a flat
field and a group path collide (`vat_number` vs. a `vat` group containing
`number`) the flat field wins; that schema is ambiguous on export too, since
both produce a single `vat_number` column.

Fixes #17118
@github-actions

Copy link
Copy Markdown
Contributor

📦 esbuild Bundle Analysis for payload

This analysis was generated by esbuild-bundle-analyzer. 🤖

Meta File Out File Size (raw) Note
packages/next/meta_index.json esbuild/index.js 201.89 KB ✅ No change
packages/payload/meta_index.json esbuild/index.js 1.41 MB ⚠️ +3.71 KB (+0.3%)
packages/payload/meta_shared.json esbuild/exports/shared.js 213.15 KB ✅ -53 B (-0.0%)
packages/richtext-lexical/meta_client.json esbuild/exports/client_optimized/index.js 285.56 KB ✅ No change
packages/ui/meta_client.json esbuild/exports/client_optimized/index.js 36.44 KB ✅ No change
packages/ui/meta_shared.json esbuild/exports/shared_optimized/index.js 18.95 KB ✅ No change
Largest paths These visualization shows top 20 largest paths in the bundle.

Meta file: packages/next/meta_index.json, Out file: esbuild/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ████████████████████████▋ }}}$ 98.9%, 197.86 KB
dist/adapters/router.js ${{\color{Goldenrod}{ }}}$ 0.4%, 718 B
dist/adapters/server.js ${{\color{Goldenrod}{ }}}$ 0.3%, 533 B
dist/adapters/layout.js ${{\color{Goldenrod}{ }}}$ 0.3%, 526 B
dist/adapters/views.js ${{\color{Goldenrod}{ }}}$ 0.2%, 409 B
dist/esbuildEntry.js ${{\color{Goldenrod}{ }}}$ 0.0%, 0 B

Meta file: packages/payload/meta_index.json, Out file: esbuild/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ █████████████████▏ }}}$ 68.6%, 964.38 KB
dist/fields/hooks ${{\color{Goldenrod}{ ▊ }}}$ 3.2%, 44.37 KB
dist/collections/operations ${{\color{Goldenrod}{ ▊ }}}$ 3.0%, 42.74 KB
dist/utilities/configToJSONSchema.js ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 15.99 KB
dist/auth/operations ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 15.68 KB
dist/queues/operations ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 14.34 KB
dist/fields/config ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 13.74 KB
dist/globals/operations ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 13.36 KB
dist/fields/validations.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 10.68 KB
dist/bin/generateImportMap ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 9.84 KB
dist/collections/config ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 9.30 KB
dist/config/orderable ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 8.07 KB
dist/index.js ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.96 KB
dist/uploads/fetchAPI-multipart ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.84 KB
dist/hierarchy/utils ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 7.64 KB
dist/database/migrations ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 7.55 KB
dist/config/sanitize.js ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 7.16 KB
dist/collections/endpoints ${{\color{Goldenrod}{ }}}$ 0.4%, 6.12 KB
dist/queues/config ${{\color{Goldenrod}{ }}}$ 0.4%, 5.68 KB
dist/uploads/endpoints ${{\color{Goldenrod}{ }}}$ 0.4%, 5.56 KB
(other) ${{\color{Goldenrod}{ ███████▊ }}}$ 31.4%, 441.09 KB

Meta file: packages/payload/meta_shared.json, Out file: esbuild/exports/shared.js

Path Size
../../node_modules ${{\color{Goldenrod}{ █████████████████▉ }}}$ 71.9%, 150.13 KB
dist/fields/validations.js ${{\color{Goldenrod}{ █▎ }}}$ 5.1%, 10.68 KB
dist/fields/config ${{\color{Goldenrod}{ ▋ }}}$ 2.8%, 5.82 KB
dist/utilities/traverseFields.js ${{\color{Goldenrod}{ ▌ }}}$ 2.1%, 4.44 KB
dist/collections/config ${{\color{Goldenrod}{ ▍ }}}$ 1.6%, 3.32 KB
dist/config/orderable ${{\color{Goldenrod}{ ▍ }}}$ 1.5%, 3.13 KB
dist/fields/baseFields ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 2.79 KB
dist/utilities/deepCopyObject.js ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 2.69 KB
dist/config/client.js ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 2.68 KB
dist/auth/cookies.js ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 1.55 KB
dist/utilities/flattenTopLevelFields.js ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 1.41 KB
dist/utilities/getVersionsConfig.js ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 1.04 KB
dist/globals/config ${{\color{Goldenrod}{ }}}$ 0.4%, 939 B
dist/utilities/flattenAllFields.js ${{\color{Goldenrod}{ }}}$ 0.4%, 793 B
dist/utilities/unflatten.js ${{\color{Goldenrod}{ }}}$ 0.4%, 779 B
dist/utilities/sanitizeUserDataForEmail.js ${{\color{Goldenrod}{ }}}$ 0.3%, 713 B
dist/auth/extractJWT.js ${{\color{Goldenrod}{ }}}$ 0.3%, 696 B
dist/utilities/getFieldPermissions.js ${{\color{Goldenrod}{ }}}$ 0.3%, 651 B
dist/errors/ValidationError.js ${{\color{Goldenrod}{ }}}$ 0.3%, 577 B
dist/bin/generateImportMap ${{\color{Goldenrod}{ }}}$ 0.3%, 561 B
(other) ${{\color{Goldenrod}{ ███████ }}}$ 28.1%, 58.54 KB

Meta file: packages/richtext-lexical/meta_client.json, Out file: esbuild/exports/client_optimized/index.js

Path Size
dist/features/blocks ${{\color{Goldenrod}{ ███▎ }}}$ 13.2%, 37.20 KB
dist/lexical/ui ${{\color{Goldenrod}{ ███ }}}$ 12.1%, 34.16 KB
dist/lexical/plugins ${{\color{Goldenrod}{ ██▉ }}}$ 11.8%, 33.18 KB
dist/features/experimental_table ${{\color{Goldenrod}{ ██▍ }}}$ 9.6%, 27.22 KB
dist/features/link ${{\color{Goldenrod}{ █▋ }}}$ 6.7%, 18.82 KB
dist/features/toolbars ${{\color{Goldenrod}{ █▍ }}}$ 5.9%, 16.58 KB
dist/features/upload ${{\color{Goldenrod}{ █▎ }}}$ 5.0%, 14.09 KB
dist/features/textState ${{\color{Goldenrod}{ ▉ }}}$ 3.9%, 11.08 KB
dist/lexical/utils ${{\color{Goldenrod}{ ▉ }}}$ 3.5%, 10.02 KB
dist/features/relationship ${{\color{Goldenrod}{ ▊ }}}$ 3.4%, 9.61 KB
dist/features/converters ${{\color{Goldenrod}{ ▊ }}}$ 3.0%, 8.36 KB
dist/utilities/fieldsDrawer ${{\color{Goldenrod}{ ▋ }}}$ 2.9%, 8.12 KB
dist/features/debug ${{\color{Goldenrod}{ ▋ }}}$ 2.6%, 7.40 KB
dist/lexical/config ${{\color{Goldenrod}{ ▍ }}}$ 1.8%, 5.14 KB
dist/features/lists ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 3.64 KB
dist/features/format ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 3.28 KB
dist/lexical/LexicalEditor.js ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 3.23 KB
dist/features/horizontalRule ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 3.18 KB
dist/field/Field.js ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 2.88 KB
dist/lexical/nodes ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 2.66 KB
(other) ${{\color{Goldenrod}{ █████████████████████▋ }}}$ 86.8%, 245.15 KB

Meta file: packages/ui/meta_client.json, Out file: esbuild/exports/client_optimized/index.js

Path Size
dist/exports/client ${{\color{Goldenrod}{ █████████████████████████ }}}$ 100.0%, 26.82 KB

Meta file: packages/ui/meta_shared.json, Out file: esbuild/exports/shared_optimized/index.js

Path Size
dist/graphics/Logo ${{\color{Goldenrod}{ ███████▋ }}}$ 30.5%, 5.57 KB
../../node_modules ${{\color{Goldenrod}{ ███▌ }}}$ 14.5%, 2.65 KB
dist/graphics/Icon ${{\color{Goldenrod}{ ██ }}}$ 8.3%, 1.51 KB
dist/utilities/formatDocTitle ${{\color{Goldenrod}{ █▊ }}}$ 7.2%, 1.32 KB
dist/providers/TableColumns ${{\color{Goldenrod}{ █▏ }}}$ 4.7%, 866 B
dist/utilities/getGlobalData.js ${{\color{Goldenrod}{ █ }}}$ 4.2%, 762 B
dist/utilities/api.js ${{\color{Goldenrod}{ █ }}}$ 4.1%, 756 B
dist/utilities/groupNavItems.js ${{\color{Goldenrod}{ █ }}}$ 4.1%, 745 B
dist/elements/Translation ${{\color{Goldenrod}{ ▋ }}}$ 2.7%, 493 B
dist/utilities/handleTakeOver.js ${{\color{Goldenrod}{ ▌ }}}$ 2.4%, 440 B
dist/utilities/traverseForLocalizedFields.js ${{\color{Goldenrod}{ ▌ }}}$ 2.3%, 419 B
dist/elements/withMergedProps ${{\color{Goldenrod}{ ▍ }}}$ 1.9%, 339 B
dist/utilities/getNavGroups.js ${{\color{Goldenrod}{ ▍ }}}$ 1.9%, 338 B
dist/utilities/getVisibleEntities.js ${{\color{Goldenrod}{ ▍ }}}$ 1.8%, 329 B
dist/elements/WithServerSideProps ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 232 B
dist/layouts/Root ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 230 B
dist/utilities/handleGoBack.js ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 180 B
dist/fields/mergeFieldStyles.js ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 158 B
dist/forms/Form ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 152 B
dist/utilities/handleBackToDashboard.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 152 B
(other) ${{\color{Goldenrod}{ █████████████████▍ }}}$ 69.5%, 12.68 KB
Details

Next to the size is how much the size has increased or decreased compared with the base branch of this PR.

  • ‼️: Size increased by 20% or more. Special attention should be given to this.
  • ⚠️: Size increased in acceptable range (lower than 20%).
  • ✅: No change or even downsized.
  • 🗑️: The out file is deleted: not found in base branch.
  • 🆕: The out file is newly found: will be added to base branch.

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.

fix(plugin-import-export): the import with CSV file doesn't populate fields correctly

1 participant