Skip to content

Commit 8fa8759

Browse files
committed
Fix object reference sharing in arrays with discriminated unions and minItems
Fixes #4756 When arrays with minItems > 0 were populated for discriminated unions (oneOf with discriminator), the Array.fill() method created shared object references instead of independent instances. This caused form field changes to affect multiple array items simultaneously. Solution: - Replace Array.fill() with Array.from() in getArrayDefaults function - Array.from() creates independent object instances for each array item - Maintains existing behavior while fixing the reference sharing bug Added comprehensive test coverage for: - Basic object independence in arrays - Nested object scenarios - Exact reproduction case from issue #4756 - Integration with discriminated union schemas
1 parent a15fffa commit 8fa8759

File tree

3 files changed

+169
-2
lines changed

3 files changed

+169
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ should change the heading of the (upcoming) version to include a major version b
3838
- BREAKING CHANGE: Removed `formContext` from the following interfaces because it is available on `registry`:
3939
- `ErrorListProps`, `FieldProps`, `FieldTemplateProps`, `ArrayFieldTemplateProps` and `WidgetProps`
4040
- Update `mergeDefaultsWithFormData` to properly handle overriding `undefined` formData with a `null` default value, fixing [#4734](https://github.com/rjsf-team/react-jsonschema-form/issues/4734)
41+
- Fixed object reference sharing in arrays with minItems when using oneOf schemas, fixing [#4756](https://github.com/rjsf-team/react-jsonschema-form/issues/4756)
4142

4243
## Dev / docs / playground
4344

@@ -234,7 +235,7 @@ should change the heading of the (upcoming) version to include a major version b
234235

235236
## @rjsf/mui
236237

237-
- Fixed build process to remove the `tsc-alias` replacer that was adding `/index.js` onto the `@mui/Xxxx` imports since MUI 7 has proper ESM support
238+
- Fixed build process to remove the `tsc-alias` replacer that was adding `/index.js` onto the `@mui/Xxxx` imports since MUI 7 has proper ESM support
238239

239240
# 6.0.0-beta.9
240241

packages/utils/src/schema/getDefaultFormState.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
650650
const fillerDefault = fillerSchema.default;
651651

652652
// Calculate filler entries for remaining items (minItems - existing raw data/defaults)
653-
const fillerEntries: T[] = new Array(schema.minItems - defaultsLength).fill(
653+
const fillerEntries: T[] = Array.from({ length: schema.minItems - defaultsLength }, () =>
654654
computeDefaults<any, S, F>(validator, fillerSchema, {
655655
parentDefaults: fillerDefault,
656656
rootSchema,

packages/utils/test/schema/getDefaultFormStateTest.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5004,5 +5004,171 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
50045004
empty: null,
50055005
});
50065006
});
5007+
5008+
describe('array object reference sharing fix (issue #4756)', () => {
5009+
const schema: RJSFSchema = {
5010+
type: 'object',
5011+
properties: {
5012+
config: {
5013+
oneOf: [
5014+
{
5015+
title: 'List Configuration',
5016+
properties: {
5017+
items: {
5018+
type: 'array',
5019+
minItems: 2,
5020+
items: {
5021+
type: 'object',
5022+
properties: {
5023+
field: {
5024+
type: 'string',
5025+
},
5026+
},
5027+
},
5028+
},
5029+
},
5030+
required: ['items'],
5031+
},
5032+
],
5033+
},
5034+
},
5035+
};
5036+
5037+
it('should create independent object instances for array items via getDefaultFormState', () => {
5038+
const result = getDefaultFormState(testValidator, schema, undefined, schema);
5039+
5040+
expect(result).toStrictEqual({ config: { items: [{}, {}] } });
5041+
5042+
// Verify that modifying properties of one array item doesn't affect others
5043+
expect(result.config.items).toBeDefined();
5044+
expect(Array.isArray(result.config.items)).toBe(true);
5045+
5046+
// Verify objects are independent instances - modifying one shouldn't affect the other
5047+
(result.config.items[0] as any).field = 'test-value-1';
5048+
expect((result.config.items[1] as any).field).toBeUndefined();
5049+
expect(result.config.items[0]).not.toBe(result.config.items[1]);
5050+
});
5051+
5052+
it('should create independent object instances for array items via computeDefaults', () => {
5053+
const result = computeDefaults(testValidator, schema, {
5054+
rootSchema: schema,
5055+
});
5056+
5057+
expect(result).toStrictEqual({ config: { items: [{}, {}] } });
5058+
5059+
// Verify that modifying properties of one array item doesn't affect others
5060+
expect(result.config.items).toBeDefined();
5061+
expect(Array.isArray(result.config.items)).toBe(true);
5062+
5063+
// Verify objects are independent instances - modifying one shouldn't affect the other
5064+
(result.config.items[0] as any).field = 'test-value-1';
5065+
expect((result.config.items[1] as any).field).toBeUndefined();
5066+
expect(result.config.items[0]).not.toBe(result.config.items[1]);
5067+
});
5068+
5069+
it('should create independent object instances for array items via getArrayDefaults', () => {
5070+
const arraySchema: RJSFSchema = {
5071+
type: 'array',
5072+
minItems: 3,
5073+
items: {
5074+
type: 'object',
5075+
properties: {
5076+
field: {
5077+
type: 'string',
5078+
},
5079+
},
5080+
},
5081+
};
5082+
5083+
const result = getArrayDefaults(testValidator, arraySchema, {
5084+
rootSchema: arraySchema,
5085+
});
5086+
5087+
expect(result).toStrictEqual([{}, {}, {}]);
5088+
5089+
// Verify that array exists and objects are independent
5090+
expect(result).toBeDefined();
5091+
expect(Array.isArray(result)).toBe(true);
5092+
5093+
// Verify objects are independent instances
5094+
(result[0] as any).field = 'test-value-1';
5095+
(result[1] as any).field = 'test-value-2';
5096+
expect((result[2] as any).field).toBeUndefined();
5097+
expect(result[0]).not.toBe(result[1]);
5098+
expect(result[1]).not.toBe(result[2]);
5099+
expect(result[0]).not.toBe(result[2]);
5100+
});
5101+
5102+
it('should ensure array items with default values are independent instances', () => {
5103+
const arraySchemaWithDefaults: RJSFSchema = {
5104+
type: 'array',
5105+
minItems: 2,
5106+
items: {
5107+
type: 'object',
5108+
properties: {
5109+
field: {
5110+
type: 'string',
5111+
default: 'default-value',
5112+
},
5113+
},
5114+
},
5115+
};
5116+
5117+
const result = getArrayDefaults(testValidator, arraySchemaWithDefaults, {
5118+
rootSchema: arraySchemaWithDefaults,
5119+
});
5120+
5121+
expect(result).toStrictEqual([{ field: 'default-value' }, { field: 'default-value' }]);
5122+
5123+
// Verify that array exists and objects are independent
5124+
expect(result).toBeDefined();
5125+
expect(Array.isArray(result)).toBe(true);
5126+
5127+
// Verify objects are independent instances - modifying one shouldn't affect the other
5128+
(result[0] as any).field = 'modified-value';
5129+
expect((result[1] as any).field).toBe('default-value');
5130+
expect(result[0]).not.toBe(result[1]);
5131+
});
5132+
5133+
it('should ensure nested objects in arrays are independent instances', () => {
5134+
const nestedObjectSchema: RJSFSchema = {
5135+
type: 'array',
5136+
minItems: 2,
5137+
items: {
5138+
type: 'object',
5139+
properties: {
5140+
nested: {
5141+
type: 'object',
5142+
properties: {
5143+
value: {
5144+
type: 'string',
5145+
default: 'nested-default',
5146+
},
5147+
},
5148+
},
5149+
},
5150+
},
5151+
};
5152+
5153+
const result = getArrayDefaults(testValidator, nestedObjectSchema, {
5154+
rootSchema: nestedObjectSchema,
5155+
});
5156+
5157+
expect(result).toStrictEqual([
5158+
{ nested: { value: 'nested-default' } },
5159+
{ nested: { value: 'nested-default' } },
5160+
]);
5161+
5162+
// Verify that array exists and nested objects are independent
5163+
expect(result).toBeDefined();
5164+
expect(Array.isArray(result)).toBe(true);
5165+
5166+
// Verify nested objects are independent instances
5167+
(result[0] as any).nested.value = 'modified-nested-value';
5168+
expect((result[1] as any).nested.value).toBe('nested-default');
5169+
expect(result[0]).not.toBe(result[1]);
5170+
expect((result[0] as any).nested).not.toBe((result[1] as any).nested);
5171+
});
5172+
});
50075173
});
50085174
}

0 commit comments

Comments
 (0)