Skip to content

Object reference sharing in arrays when using discriminated unions with minItems #4756

@The-Zona-Zoo

Description

@The-Zona-Zoo

Prerequisites

What theme are you using?

core

Version

6.x

Current Behavior

When using discriminated unions (oneOf) and switching to a schema that contains an array with minItems, RJSF creates shared object references for all array items. Modifying properties in one array item affects all other items in the array.

All array items share the same object reference. Changes to any property in one item are reflected across all items in the array.

Expected Behavior

Each array item should be an independent object with its own property values. Modifying one item should not affect other items.

Steps To Reproduce

Live reproduction

  1. Open the reproduction link above
  2. Modify the "field" value in any of the array items
  3. Bug: All other array items reflect the same changes

Environment

- OS: MacOS 15.6
- Node: 22.17.1
- npm: 10.9.2

Anything else?

Root Cause

The bug is in packages/utils/src/schema/getDefaultFormState.ts in the getArrayDefaults function around line 653:

const fillerEntries: T[] = new Array(schema.minItems - defaultsLength).fill(
  computeDefaults<any, S, F>(validator, fillerSchema, {
    // ... config
  }),
) as T[];

The .fill() method is called with a single object reference, causing all array positions to reference the same object in memory.

Critical Conditions

This bug is very specific to:

  • Discriminated unions (oneOf)
  • Undefined initial form data (not empty arrays)
  • Arrays with minItems > 0
  • Array items that are objects (not primitives)
  • Must avoid early return conditions in getArrayDefaults: not multiselect, not computeSkipPopulate, minItems > existing defaults length

Suggested Fix

Option 1: Array.from with individual computeDefaults calls (safer)

const fillerEntries: T[] = Array.from({ length: schema.minItems - defaultsLength }, () =>
  computeDefaults<any, S, F>(validator, fillerSchema, {
    parentDefaults: fillerDefault,
    rootSchema,
    _recurseList,
    experimental_defaultFormStateBehavior,
    experimental_customMergeAllOf,
    required,
    shouldMergeDefaultsIntoFormData,
  })
) as T[];

Option 2: structuredClone optimization (potentially unsafe)

const baseDefault = computeDefaults<any, S, F>(validator, fillerSchema, {
  parentDefaults: fillerDefault,
  rootSchema,
  _recurseList,
  experimental_defaultFormStateBehavior,
  experimental_customMergeAllOf,
  required,
  shouldMergeDefaultsIntoFormData,
});
const fillerEntries: T[] = Array.from({ length: schema.minItems - defaultsLength }, () =>
  structuredClone(baseDefault)
) as T[];

Closest existing issue I could find: #847

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions