Skip to content

Commit d86c174

Browse files
authored
fix: file objects being serialized to plain objects in upload form state (#14818)
### What? Fixes a bug where saving an upload document would fail with `"[object Object]" is not valid JSON` error after changing the file and then modifying another field. ### Why? When a file was changed in an upload document and saved, the File object would persist in the client-side form state. On the next save (when modifying a different field), the form state would be deep copied and serialized before submission. This serialization converted the File object into a plain object `{name, size, type, ...}`, which was then stringified to `"[object Object]"` and sent to the server, causing a JSON parsing error. ### How? 1. **Preserve File objects during serialization** - Added instanceof File check in `deepCopyObjectSimpleWithoutReactComponents` to return File objects as-is instead of serializing them to plain objects - Similar to how Date objects are already handled 2. **Clear file field after successful save** - Delete the `file` field from form state after save in upload collections - Prevents stale File objects from persisting between saves I could of just added an instanceof File check in [createFormData](https://github.com/payloadcms/payload/blob/1340818097c90904ff9e0facb5b30329bbeb430e/packages/ui/src/forms/Form/index.tsx#L528) but it would be just a bandaid fix. If a non-File object reaches that code, it indicates a bug elsewhere that should fail loudly. The root causes were: - File objects being incorrectly serialized during form submission - File objects persisting in form state after they should be cleared Fixes #14814
1 parent db13a60 commit d86c174

File tree

5 files changed

+37
-0
lines changed

5 files changed

+37
-0
lines changed

packages/payload/src/utilities/deepCopyObject.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@ export function deepCopyObjectSimpleWithoutReactComponents<T extends JsonValue>(
150150
typeof e !== 'object' || e === null ? e : deepCopyObjectSimpleWithoutReactComponents(e),
151151
) as T
152152
} else {
153+
// Handle File objects by returning them as-is (don't serialize to plain object)
154+
if (value instanceof File) {
155+
return value as unknown as T
156+
}
153157
if (value instanceof Date) {
154158
return new Date(value) as unknown as T
155159
}

packages/ui/src/views/Edit/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,12 @@ export function DefaultEditView({
353353
skipValidation: true,
354354
})
355355

356+
// For upload collections, clear the file field from the returned state
357+
// to prevent the File object from persisting in form state after save
358+
if (upload && state) {
359+
delete state.file
360+
}
361+
356362
// Unlock the document after save
357363
if (isLockingEnabled) {
358364
setDocumentIsLocked(false)

test/uploads/e2e.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1842,4 +1842,29 @@ describe('Uploads', () => {
18421842
await expect(menuList.getByText('Sizes > four > File Size', { exact: true })).toHaveCount(1)
18431843
await expect(menuList.getByText('Sizes > four > File Name', { exact: true })).toHaveCount(1)
18441844
})
1845+
1846+
test('should allow saving other fields after changing file', async () => {
1847+
await page.goto(uploadsTwo.create)
1848+
1849+
// Upload initial file with required field
1850+
await page.setInputFiles('input[type="file"]', path.resolve(dirname, './image.png'))
1851+
await page.locator('#field-prefix').fill('initial')
1852+
await saveDocAndAssert(page)
1853+
1854+
await page.locator('button[data-close-button="true"]').click()
1855+
1856+
// Change the file
1857+
await page.locator('.file-details__remove').click()
1858+
await page.setInputFiles('input[type="file"]', path.resolve(dirname, './test-image.jpg'))
1859+
await saveDocAndAssert(page)
1860+
1861+
await page.locator('button[data-close-button="true"]').click()
1862+
1863+
// Modify another field and save - this should work without errors
1864+
await page.locator('#field-title').fill('updated title')
1865+
await saveDocAndAssert(page)
1866+
1867+
const titleField = page.locator('#field-title')
1868+
await expect(titleField).toHaveValue('updated title')
1869+
})
18451870
})

test/uploads/payload-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ export interface Config {
198198
db: {
199199
defaultIDType: string;
200200
};
201+
fallbackLocale: ('false' | 'none' | 'null') | false | null | ('en' | 'es' | 'fr') | ('en' | 'es' | 'fr')[];
201202
globals: {};
202203
globalsSelect: {};
203204
locale: 'en' | 'es' | 'fr';

test/uploads/shared.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,4 @@ export const svgOnlySlug = 'svg-only'
4444
export const anyImagesSlug = 'any-images'
4545
export const mediaWithoutDeleteAccessSlug = 'media-without-delete-access'
4646
export const mediaWithImageSizeAdminPropsSlug = 'media-with-image-size-admin-props'
47+
export const uploads2Slug = 'uploads-2'

0 commit comments

Comments
 (0)