From 51c970568c9ddb85bbf738716aee6735b614ada8 Mon Sep 17 00:00:00 2001 From: Vijay Misal Date: Fri, 7 Aug 2026 22:38:22 +0530 Subject: [PATCH] fix(payload): preserve nested arrays when cloning omitted json field data cloneDataFromOriginalDoc shallow-cloned every array row with `{...row}`, which is correct for plain objects but silently turns nested arrays into index-keyed objects (`[1, 2]` -> `{ "0": 1, "1": 2 }`). This runs whenever a partial update omits a field and its previous value is carried forward via getFallbackValue, so a json field storing an array-of-arrays (e.g. coordinate tuples) gets corrupted on any update that doesn't touch it. Draft saves skip validation entirely, so the corrupted shape persists. Recurse into nested arrays instead of spreading them as objects, so array shape is preserved at any depth while existing object-cloning behavior is unchanged. Fixes #17475 --- .../beforeChange/cloneDataFromOriginalDoc.ts | 6 ++++ test/fields/int.spec.ts | 30 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/packages/payload/src/fields/hooks/beforeChange/cloneDataFromOriginalDoc.ts b/packages/payload/src/fields/hooks/beforeChange/cloneDataFromOriginalDoc.ts index 0aadae2dc34..1db456767c4 100644 --- a/packages/payload/src/fields/hooks/beforeChange/cloneDataFromOriginalDoc.ts +++ b/packages/payload/src/fields/hooks/beforeChange/cloneDataFromOriginalDoc.ts @@ -5,6 +5,12 @@ export const cloneDataFromOriginalDoc = ( ): JsonArray | JsonObject => { if (Array.isArray(originalDocData)) { return originalDocData.map((row) => { + // Recurse for nested arrays so they stay arrays instead of being + // spread into index-keyed objects (`{...[1, 2]}` -> `{ 0: 1, 1: 2 }`). + if (Array.isArray(row)) { + return cloneDataFromOriginalDoc(row) + } + if (typeof row === 'object' && row != null) { return { ...row, diff --git a/test/fields/int.spec.ts b/test/fields/int.spec.ts index 1c1a720fa4a..c97672f3a07 100644 --- a/test/fields/int.spec.ts +++ b/test/fields/int.spec.ts @@ -4318,6 +4318,36 @@ describe('Fields', () => { expect(updatedJsonFieldsDoc.json.state).toEqual({}) }) + it('should preserve nested arrays in a json field omitted from a partial update', async () => { + const arrayOfArrays = [ + [1, 2], + [3, 4], + ] + + const jsonFieldsDoc = await payload.create({ + collection: 'json-fields', + data: { + customJSON: arrayOfArrays, + }, + }) + + expect(jsonFieldsDoc.customJSON).toStrictEqual(arrayOfArrays) + + // Partial update that intentionally omits `customJSON` so that its + // existing value is cloned from the original doc via + // `cloneDataFromOriginalDoc`. Nested arrays must survive that clone + // instead of being turned into index-keyed objects. + const updatedJsonFieldsDoc = await payload.update({ + id: jsonFieldsDoc.id, + collection: 'json-fields', + data: { + json: { foo: 'bar' }, + }, + }) + + expect(updatedJsonFieldsDoc.customJSON).toStrictEqual(arrayOfArrays) + }) + describe('querying', () => { let fooBar let bazBar