From 07436477a63156316f319be7a7317b1d18bb2a32 Mon Sep 17 00:00:00 2001 From: Adam Haeger Date: Mon, 1 Jun 2026 12:18:00 +0200 Subject: [PATCH 1/5] fixed --- src/features/formData/FormData.test.tsx | 69 +++++++++++++++++++ .../formData/FormDataWriteStateMachine.tsx | 5 ++ 2 files changed, 74 insertions(+) diff --git a/src/features/formData/FormData.test.tsx b/src/features/formData/FormData.test.tsx index d8d0276dcc..dd4ff820e3 100644 --- a/src/features/formData/FormData.test.tsx +++ b/src/features/formData/FormData.test.tsx @@ -21,6 +21,7 @@ import { RulesProvider } from 'src/features/form/rules/RulesContext'; import { GlobalFormDataReadersProvider } from 'src/features/formData/FormDataReaders'; import { FD, FormDataWriteProvider } from 'src/features/formData/FormDataWrite'; import { FormDataWriteProxyProvider } from 'src/features/formData/FormDataWriteProxies'; +import { DEFAULT_DEBOUNCE_TIMEOUT } from 'src/features/formData/types'; import { useDataModelBindings } from 'src/features/formData/useDataModelBindings'; import { fetchApplicationMetadata } from 'src/queries/queries'; import { @@ -753,4 +754,72 @@ describe('FormData', () => { expect(screen.getByTestId('valid-obj3.prop1')).toHaveValue(''); }); }); + + // Regression test for https://github.com/Altinn/app-frontend-react/issues/4053 + // + // A FileUpload with `dataModelBindings.list` inside a RepeatingGroup writes the whole list of + // attachment IDs back into the data model (see MaintainListDataModelBinding) by calling + // `setValue('list', [...])`, which routes through `setLeafValue`. When that list field is already + // pre-populated with 2+ values, the underlying `dot.str` call tries to redefine the existing + // non-empty array and throws `Trying to redefine non-empty obj['attachmentId']`, which crashes + // node generation for that component. This test reproduces that write through the same public hook. + describe('List data model bindings (issue #4053)', () => { + const listSchema: JSONSchema7 = { + type: 'object', + properties: { + designs: { + type: 'array', + items: { + type: 'object', + properties: { + attachmentId: { + type: 'array', + items: { type: 'string' }, + }, + }, + }, + }, + }, + }; + + function ListWriter() { + const { formData, setValue } = useDataModelBindings( + { list: { field: 'designs[0].attachmentId', dataType: defaultDataTypeMock } }, + DEFAULT_DEBOUNCE_TIMEOUT, + 'raw', + ); + const list = (formData.list ?? []) as string[]; + + return ( + <> +
{list.join(',')}
+ + + ); + } + + async function render() { + return statefulRender({ + renderer: , + queries: { + fetchDataModelSchema: async () => listSchema, + fetchFormData: async () => ({ designs: [{ attachmentId: ['id-1', 'id-2'] }] }), + }, + }); + } + + it('writing a list binding that is already pre-populated with 2+ values should not throw', async () => { + const user = userEvent.setup(); + await render(); + + expect(screen.getByTestId('list-value')).toHaveTextContent('id-1,id-2'); + + // This is what MaintainListDataModelBinding does on load when the order of the mapped + // attachments differs from the order stored in the data model. Before the fix this throws + // "Trying to redefine non-empty obj['attachmentId']". + await user.click(screen.getByRole('button', { name: 'Update list' })); + + await waitFor(() => expect(screen.getByTestId('list-value')).toHaveTextContent('id-2,id-1')); + }); + }); }); diff --git a/src/features/formData/FormDataWriteStateMachine.tsx b/src/features/formData/FormDataWriteStateMachine.tsx index 22a5bf11c9..2eef7c35c7 100644 --- a/src/features/formData/FormDataWriteStateMachine.tsx +++ b/src/features/formData/FormDataWriteStateMachine.tsx @@ -367,6 +367,11 @@ function makeActions( dot.str(reference.field, newValue, state.dataModels[reference.dataType].invalidCurrentData); } else { dot.delete(reference.field, state.dataModels[reference.dataType].invalidCurrentData); + // Delete any existing value before writing. `dot.str` (with override disabled) throws + // "Trying to redefine non-empty obj[...]" when the target path already holds a non-empty + // array/object, which happens e.g. when a FileUpload `list` binding is pre-populated with + // 2+ attachment IDs and we write the reconciled list back. See issue #4053. + dot.delete(reference.field, state.dataModels[reference.dataType].currentData); dot.str(reference.field, convertedValue, state.dataModels[reference.dataType].currentData); } return { newValue, convertedValue, error, hadError }; From 7c72f69db26b05b1f965135fb3683522a44e089c Mon Sep 17 00:00:00 2001 From: Adam Haeger Date: Mon, 1 Jun 2026 12:26:22 +0200 Subject: [PATCH 2/5] cleaned up coments --- src/features/formData/FormData.test.tsx | 9 +-------- src/features/formData/FormDataWriteStateMachine.tsx | 4 ---- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/features/formData/FormData.test.tsx b/src/features/formData/FormData.test.tsx index dd4ff820e3..193c4acc78 100644 --- a/src/features/formData/FormData.test.tsx +++ b/src/features/formData/FormData.test.tsx @@ -756,14 +756,7 @@ describe('FormData', () => { }); // Regression test for https://github.com/Altinn/app-frontend-react/issues/4053 - // - // A FileUpload with `dataModelBindings.list` inside a RepeatingGroup writes the whole list of - // attachment IDs back into the data model (see MaintainListDataModelBinding) by calling - // `setValue('list', [...])`, which routes through `setLeafValue`. When that list field is already - // pre-populated with 2+ values, the underlying `dot.str` call tries to redefine the existing - // non-empty array and throws `Trying to redefine non-empty obj['attachmentId']`, which crashes - // node generation for that component. This test reproduces that write through the same public hook. - describe('List data model bindings (issue #4053)', () => { + describe('List data model bindings regression, issue #4053', () => { const listSchema: JSONSchema7 = { type: 'object', properties: { diff --git a/src/features/formData/FormDataWriteStateMachine.tsx b/src/features/formData/FormDataWriteStateMachine.tsx index 2eef7c35c7..1e959c8bb3 100644 --- a/src/features/formData/FormDataWriteStateMachine.tsx +++ b/src/features/formData/FormDataWriteStateMachine.tsx @@ -367,10 +367,6 @@ function makeActions( dot.str(reference.field, newValue, state.dataModels[reference.dataType].invalidCurrentData); } else { dot.delete(reference.field, state.dataModels[reference.dataType].invalidCurrentData); - // Delete any existing value before writing. `dot.str` (with override disabled) throws - // "Trying to redefine non-empty obj[...]" when the target path already holds a non-empty - // array/object, which happens e.g. when a FileUpload `list` binding is pre-populated with - // 2+ attachment IDs and we write the reconciled list back. See issue #4053. dot.delete(reference.field, state.dataModels[reference.dataType].currentData); dot.str(reference.field, convertedValue, state.dataModels[reference.dataType].currentData); } From a2d6d0f6d4eaae5ddc9c91daf6a3acd650c84c11 Mon Sep 17 00:00:00 2001 From: Adam Haeger Date: Mon, 1 Jun 2026 12:41:42 +0200 Subject: [PATCH 3/5] removed casting --- src/features/formData/FormData.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/formData/FormData.test.tsx b/src/features/formData/FormData.test.tsx index 193c4acc78..9dfcbb968b 100644 --- a/src/features/formData/FormData.test.tsx +++ b/src/features/formData/FormData.test.tsx @@ -781,7 +781,7 @@ describe('FormData', () => { DEFAULT_DEBOUNCE_TIMEOUT, 'raw', ); - const list = (formData.list ?? []) as string[]; + const list = formData.list ?? []; return ( <> From d69488afd8b338b5204c7e8f3249c95ee1b3571b Mon Sep 17 00:00:00 2001 From: Adam Haeger Date: Mon, 1 Jun 2026 13:13:06 +0200 Subject: [PATCH 4/5] reverted to casting to string[] --- src/features/formData/FormData.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/formData/FormData.test.tsx b/src/features/formData/FormData.test.tsx index 9dfcbb968b..193c4acc78 100644 --- a/src/features/formData/FormData.test.tsx +++ b/src/features/formData/FormData.test.tsx @@ -781,7 +781,7 @@ describe('FormData', () => { DEFAULT_DEBOUNCE_TIMEOUT, 'raw', ); - const list = formData.list ?? []; + const list = (formData.list ?? []) as string[]; return ( <> From 847232f807e7612d89efc4440d605e61618e6e2e Mon Sep 17 00:00:00 2001 From: Adam Haeger Date: Mon, 1 Jun 2026 14:35:33 +0200 Subject: [PATCH 5/5] trigger build --- src/features/formData/FormData.test.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/features/formData/FormData.test.tsx b/src/features/formData/FormData.test.tsx index 193c4acc78..5bde53479c 100644 --- a/src/features/formData/FormData.test.tsx +++ b/src/features/formData/FormData.test.tsx @@ -804,9 +804,7 @@ describe('FormData', () => { it('writing a list binding that is already pre-populated with 2+ values should not throw', async () => { const user = userEvent.setup(); await render(); - expect(screen.getByTestId('list-value')).toHaveTextContent('id-1,id-2'); - // This is what MaintainListDataModelBinding does on load when the order of the mapped // attachments differs from the order stored in the data model. Before the fix this throws // "Trying to redefine non-empty obj['attachmentId']".