Skip to content

feat: allow override form values with defaults #4625

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ should change the heading of the (upcoming) version to include a major version b

-->

# 6.0.0-beta.9

## @rjsf/util

- Allow form value overrides with defaults [#4625](https://github.com/rjsf-team/react-jsonschema-form/pull/4625
Copy link
Member

@heath-freenome heath-freenome May 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to move to a new # 6.0.0-beta.11 section since we released this one already


# 6.0.0-beta.8

## @rjsf/util
Expand Down
3 changes: 3 additions & 0 deletions packages/docs/docs/api-reference/form-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,9 @@ NOTE: If there is a default for a field and the `formData` is unspecified, the d
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `useFormDataIfPresent` | Legacy behavior - Do not merge defaults if there is a value for a field in `formData` even if that value is explicitly set to `undefined` |
| `useDefaultIfFormDataUndefined` | If the value of a field within the `formData` is `undefined`, then use the default value instead |
| `useDefaultAlways` | Always use the default value instead of form data |

|

## experimental_customMergeAllOf

Expand Down
3 changes: 2 additions & 1 deletion packages/docs/docs/api-reference/utility-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,7 @@ Merges the `defaults` object of type `T` into the `formData` of type `T`
When merging defaults and form data, we want to merge in this specific way:

- objects are deeply merged
- arrays are merged in such a way that:
- arrays are either replaced (when `defaultSupercedes` is true) or merged in such a way that:
- when the array is set in form data, only array entries set in form data are deeply merged; additional entries from the defaults are ignored unless `mergeExtraArrayDefaults` is true, in which case the extras are appended onto the end of the form data
- when the array is not set in form data, the default is copied over
- scalars are overwritten/set by form data
Expand All @@ -672,6 +672,7 @@ When merging defaults and form data, we want to merge in this specific way:
- [formData]: T | undefined - The form data into which the defaults will be merged
- [mergeExtraArrayDefaults=false]: boolean - If true, any additional default array entries are appended onto the formData
- [defaultSupercedesUndefined=false]: boolean - If true, an explicit undefined value will be overwritten by the default value
- [defaultSupercedes=false]: boolean - If true, a value will be overwritten by the default value

#### Returns

Expand Down
5 changes: 5 additions & 0 deletions packages/playground/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,11 @@ const liveSettingsSelectSchema: RJSFSchema = {
title: 'Use default for undefined field value',
enum: ['useDefaultIfFormDataUndefined'],
},
{
type: 'string',
title: 'Always use default for field value',
enum: ['useDefaultAlways'],
},
],
},
},
Expand Down
46 changes: 28 additions & 18 deletions packages/utils/src/mergeDefaultsWithFormData.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import get from 'lodash/get';

import isObject from './isObject';
import { GenericObjectType } from '../src';
import { GenericObjectType, OverrideFormDataStrategy } from '../src';
import isNil from 'lodash/isNil';

/** Merges the `defaults` object of type `T` into the `formData` of type `T`
Expand All @@ -20,25 +20,24 @@ import isNil from 'lodash/isNil';
* @param [formData] - The form data into which the defaults will be merged
* @param [mergeExtraArrayDefaults=false] - If true, any additional default array entries are appended onto the formData
* @param [defaultSupercedesUndefined=false] - If true, an explicit undefined value will be overwritten by the default value
* @param [overrideFormDataWithDefaults=false] - If true, the default value will overwrite the form data value. If the value
* doesn't exist in the default, we take it from formData and in the case where the value is set to undefined in formData.
* This is useful when we have already merged formData with defaults and want to add an additional field from formData
* that does not exist in defaults.
* @param [overrideFormDataWithDefaultsStrategy=OverrideFormDataStrategy.noop] - Strategy for merging defaults and form data
* @returns - The resulting merged form data with defaults
*/
export default function mergeDefaultsWithFormData<T = any>(
defaults?: T,
formData?: T,
mergeExtraArrayDefaults = false,
defaultSupercedesUndefined = false,
overrideFormDataWithDefaults = false,
overrideFormDataWithDefaultsStrategy: OverrideFormDataStrategy = OverrideFormDataStrategy.noop,
): T | undefined {
if (Array.isArray(formData)) {
const defaultsArray = Array.isArray(defaults) ? defaults : [];

// If overrideFormDataWithDefaults is true, we want to override the formData with the defaults
const overrideArray = overrideFormDataWithDefaults ? defaultsArray : formData;
const overrideOppositeArray = overrideFormDataWithDefaults ? formData : defaultsArray;
// If overrideFormDataWithDefaultsStrategy is not noop, we want to override the formData with the defaults
const overrideArray =
overrideFormDataWithDefaultsStrategy !== OverrideFormDataStrategy.noop ? defaultsArray : formData;
const overrideOppositeArray =
overrideFormDataWithDefaultsStrategy !== OverrideFormDataStrategy.noop ? formData : defaultsArray;

const mapped = overrideArray.map((value, idx) => {
// We want to explicitly make sure that the value is NOT undefined since null, 0 and empty space are valid values
Expand All @@ -48,33 +47,44 @@ export default function mergeDefaultsWithFormData<T = any>(
formData[idx],
mergeExtraArrayDefaults,
defaultSupercedesUndefined,
overrideFormDataWithDefaults,
overrideFormDataWithDefaultsStrategy,
);
}
return value;
});

// Merge any extra defaults when mergeExtraArrayDefaults is true
// Or when overrideFormDataWithDefaults is true and the default array is shorter than the formData array
if ((mergeExtraArrayDefaults || overrideFormDataWithDefaults) && mapped.length < overrideOppositeArray.length) {
// Or when overrideFormDataWithDefaults is 'merge' and the default array is shorter than the formData array
if (
(mergeExtraArrayDefaults || overrideFormDataWithDefaultsStrategy === OverrideFormDataStrategy.merge) &&
mapped.length < overrideOppositeArray.length
) {
mapped.push(...overrideOppositeArray.slice(mapped.length));
}
return mapped as unknown as T;
}
if (isObject(formData)) {
const iterationSource =
overrideFormDataWithDefaultsStrategy === OverrideFormDataStrategy.replace ? (defaults ?? {}) : formData;
const acc: { [key in keyof T]: any } = Object.assign({}, defaults); // Prevent mutation of source object.
return Object.keys(formData as GenericObjectType).reduce((acc, key) => {
return Object.keys(iterationSource as GenericObjectType).reduce((acc, key) => {
const keyValue = get(formData, key);
const keyExistsInDefaults = isObject(defaults) && key in (defaults as GenericObjectType);
const keyExistsInFormData = key in (formData as GenericObjectType);
// overrideFormDataWithDefaultsStrategy can be 'merge' only when the key value exists in defaults
// Or if the key value doesn't exist in formData
const keyOverrideDefaultStrategy =
overrideFormDataWithDefaultsStrategy === OverrideFormDataStrategy.replace
? OverrideFormDataStrategy.replace
: keyExistsInDefaults || !keyExistsInFormData
? overrideFormDataWithDefaultsStrategy
: OverrideFormDataStrategy.noop;
acc[key as keyof T] = mergeDefaultsWithFormData<T>(
defaults ? get(defaults, key) : {},
keyValue,
mergeExtraArrayDefaults,
defaultSupercedesUndefined,
// overrideFormDataWithDefaults can be true only when the key value exists in defaults
// Or if the key value doesn't exist in formData
overrideFormDataWithDefaults && (keyExistsInDefaults || !keyExistsInFormData),
keyOverrideDefaultStrategy,
);
return acc;
}, acc);
Expand All @@ -89,10 +99,10 @@ export default function mergeDefaultsWithFormData<T = any>(
if (
(defaultSupercedesUndefined &&
((!isNil(defaults) && isNil(formData)) || (typeof formData === 'number' && isNaN(formData)))) ||
(overrideFormDataWithDefaults && !isNil(formData))
(overrideFormDataWithDefaultsStrategy === OverrideFormDataStrategy.merge && !isNil(formData))
) {
return defaults;
}

return formData;
return overrideFormDataWithDefaultsStrategy === OverrideFormDataStrategy.replace ? defaults : formData;
}
6 changes: 5 additions & 1 deletion packages/utils/src/schema/getDefaultFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
Experimental_DefaultFormStateBehavior,
FormContextType,
GenericObjectType,
OverrideFormDataStrategy,
RJSFSchema,
StrictRJSFSchema,
ValidatorType,
Expand Down Expand Up @@ -363,6 +364,9 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
matchingFormData as T,
mergeExtraDefaults,
true,
experimental_defaultFormStateBehavior?.mergeDefaultsIntoFormData === 'useDefaultAlways'
? OverrideFormDataStrategy.replace
: OverrideFormDataStrategy.noop,
) as T;
}
}
Expand Down Expand Up @@ -732,7 +736,7 @@ export default function getDefaultFormState<
formData,
true, // set to true to add any additional default array entries.
defaultSupercedesUndefined,
true, // set to true to override formData with defaults if they exist.
OverrideFormDataStrategy.merge, // set to 'merge' to override formData with defaults if they exist.
);
return result;
}
Expand Down
14 changes: 13 additions & 1 deletion packages/utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,9 @@ export type Experimental_DefaultFormStateBehavior = {
* even if that value is explicitly set to `undefined`
* - `useDefaultIfFormDataUndefined`: - If the value of a field within the `formData` is `undefined`, then use the
* default value instead
* - `useDefaultAlways`: - Always use the default value
*/
mergeDefaultsIntoFormData?: 'useFormDataIfPresent' | 'useDefaultIfFormDataUndefined';
mergeDefaultsIntoFormData?: 'useFormDataIfPresent' | 'useDefaultIfFormDataUndefined' | 'useDefaultAlways';
/** Optional enumerated flag controlling how const values are merged into the form data as defaults when dealing with
* undefined values, defaulting to `always`. The defaulting behavior for this flag will always be controlled by the
* `emptyObjectField` flag value. For instance, if `populateRequiredDefaults` is set and the const value is not
Expand Down Expand Up @@ -1265,3 +1266,14 @@ export interface SchemaUtilsType<T = any, S extends StrictRJSFSchema = RJSFSchem
*/
toPathSchema(schema: S, name?: string, formData?: T): PathSchema<T>;
}

/** Strategy for merging defaults with existing form data */
export enum OverrideFormDataStrategy {
/** No merge or override applied */
noop,
/** If the value doesn't exist in the default, we take it from formData and in the case where the value is set to undefined in formData.
* This is useful when we have already merged formData with defaults and want to add an additional field from formData that does not exist in defaults */
merge,
/** Replace form data with defined default */
replace,
}
Loading
Loading