Skip to content

Commit ca2ddc2

Browse files
Fix 1581 by supporting null defaults for object and array (#4771)
Fixed #1581 by fixing `getDefaultFormState()` to support `null` defaults for `object` and `array` types - Updated `getDefaultFormState()` to add `computeDefaultBasedOnSchemaTypeAndDefaults()` to detect when `["null", "object"|"array"]`, default as `null` and the computed default was empty - Updated the tests for `getDefaultFormState()` to tests all of the `computeDefaultBasedOnSchemaTypeAndDefaults()` - Updated `CHANGELOG.md` accordingly
1 parent a3a244c commit ca2ddc2

File tree

3 files changed

+178
-40
lines changed

3 files changed

+178
-40
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ should change the heading of the (upcoming) version to include a major version b
2626

2727
- Update `ArrayFieldItemTemplate` to align buttons with the input field, fixing [#4753](https://github.com/rjsf-team/react-jsonschema-form/pull/4753)
2828

29+
## @rjsf/utils
30+
31+
- Update `getDefaultFormState()` to add support for `null` defaults for `["null", "object"]` and `["null", "array"]`, fixing [#1581](https://github.com/rjsf-team/react-jsonschema-form/issues/1581)
32+
2933
# 6.0.0-beta.16
3034

3135
## @rjsf/antd

packages/utils/src/schema/getDefaultFormState.ts

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,24 @@ export function getInnerSchemaForArrayItem<S extends StrictRJSFSchema = RJSFSche
8484
return {} as S;
8585
}
8686

87+
/** Checks if the given `schema` contains the `null` type along with another type AND if the `default` contained within
88+
* the schema is `null` AND the `computedDefault` is empty. If all of those conditions are true, then the `schema`'s
89+
* default should be `null` rather than `computedDefault`.
90+
*
91+
* @param schema - The schema to inspect
92+
* @param computedDefault - The computed default for the schema
93+
* @returns - Flag indicating whether a null should be returned instead of the computedDefault
94+
*/
95+
export function computeDefaultBasedOnSchemaTypeAndDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema>(
96+
schema: S,
97+
computedDefault: T,
98+
) {
99+
const { default: schemaDefault, type } = schema;
100+
const shouldReturnNullAsDefault =
101+
Array.isArray(type) && type.includes('null') && isEmpty(computedDefault) && schemaDefault === null;
102+
return shouldReturnNullAsDefault ? (null as T) : computedDefault;
103+
}
104+
87105
/** Either add `computedDefault` at `key` into `obj` or not add it based on its value, the value of
88106
* `includeUndefinedValues`, the value of `emptyObjectFields` and if its parent field is required. Generally undefined
89107
* `computedDefault` values are added only when `includeUndefinedValues` is either true/"excludeObjectChildren". If `
@@ -446,7 +464,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
446464
required,
447465
shouldMergeDefaultsIntoFormData,
448466
}: ComputeDefaultsProps<T, S> = {},
449-
defaults?: T | T[] | undefined,
467+
defaults?: T | T[],
450468
): T {
451469
{
452470
const formData: T = (isObject(rawFormData) ? rawFormData : {}) as T;
@@ -539,7 +557,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
539557
);
540558
});
541559
}
542-
return objectDefaults;
560+
return computeDefaultBasedOnSchemaTypeAndDefaults<T, S>(rawSchema, objectDefaults);
543561
}
544562
}
545563

@@ -563,8 +581,8 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
563581
required,
564582
shouldMergeDefaultsIntoFormData,
565583
}: ComputeDefaultsProps<T, S> = {},
566-
defaults?: T | T[] | undefined,
567-
): T | T[] | undefined {
584+
defaults?: T[],
585+
): T[] | undefined {
568586
const schema: S = rawSchema;
569587

570588
const arrayMinItemsStateBehavior = experimental_defaultFormStateBehavior?.arrayMinItems ?? {};
@@ -576,7 +594,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
576594
const computeSkipPopulate = arrayMinItemsStateBehavior?.computeSkipPopulate ?? (() => false);
577595
const isSkipEmptyDefaults = experimental_defaultFormStateBehavior?.emptyObjectFields === 'skipEmptyDefaults';
578596

579-
const emptyDefault = isSkipEmptyDefaults ? undefined : [];
597+
const emptyDefault: T[] | undefined = isSkipEmptyDefaults ? undefined : [];
580598

581599
// Inject defaults into existing array defaults
582600
if (Array.isArray(defaults)) {
@@ -598,7 +616,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
598616
if (Array.isArray(rawFormData)) {
599617
const schemaItem: S = getInnerSchemaForArrayItem<S>(schema);
600618
if (neverPopulate) {
601-
defaults = rawFormData;
619+
defaults = rawFormData as typeof defaults;
602620
} else {
603621
const itemDefaults = rawFormData.map((item: T, idx: number) => {
604622
return computeDefaults<T, S, F>(validator, schemaItem, {
@@ -635,34 +653,37 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
635653
}
636654
}
637655

656+
let arrayDefault: T[] | undefined;
638657
const defaultsLength = Array.isArray(defaults) ? defaults.length : 0;
639658
if (
640659
!schema.minItems ||
641660
isMultiSelect<T, S, F>(validator, schema, rootSchema, experimental_customMergeAllOf) ||
642661
computeSkipPopulate<T, S, F>(validator, schema, rootSchema) ||
643662
schema.minItems <= defaultsLength
644663
) {
645-
return defaults ? defaults : emptyDefault;
664+
arrayDefault = defaults ? defaults : emptyDefault;
665+
} else {
666+
const defaultEntries: T[] = (defaults || []) as T[];
667+
const fillerSchema: S = getInnerSchemaForArrayItem<S>(schema, AdditionalItemsHandling.Invert);
668+
const fillerDefault = fillerSchema.default;
669+
670+
// Calculate filler entries for remaining items (minItems - existing raw data/defaults)
671+
const fillerEntries: T[] = Array.from({ length: schema.minItems - defaultsLength }, () =>
672+
computeDefaults<any, S, F>(validator, fillerSchema, {
673+
parentDefaults: fillerDefault,
674+
rootSchema,
675+
_recurseList,
676+
experimental_defaultFormStateBehavior,
677+
experimental_customMergeAllOf,
678+
required,
679+
shouldMergeDefaultsIntoFormData,
680+
}),
681+
) as T[];
682+
// then fill up the rest with either the item default or empty, up to minItems
683+
arrayDefault = defaultEntries.concat(fillerEntries);
646684
}
647685

648-
const defaultEntries: T[] = (defaults || []) as T[];
649-
const fillerSchema: S = getInnerSchemaForArrayItem<S>(schema, AdditionalItemsHandling.Invert);
650-
const fillerDefault = fillerSchema.default;
651-
652-
// Calculate filler entries for remaining items (minItems - existing raw data/defaults)
653-
const fillerEntries: T[] = Array.from({ length: schema.minItems - defaultsLength }, () =>
654-
computeDefaults<any, S, F>(validator, fillerSchema, {
655-
parentDefaults: fillerDefault,
656-
rootSchema,
657-
_recurseList,
658-
experimental_defaultFormStateBehavior,
659-
experimental_customMergeAllOf,
660-
required,
661-
shouldMergeDefaultsIntoFormData,
662-
}),
663-
) as T[];
664-
// then fill up the rest with either the item default or empty, up to minItems
665-
return defaultEntries.concat(fillerEntries);
686+
return computeDefaultBasedOnSchemaTypeAndDefaults<T[] | undefined, S>(rawSchema, arrayDefault);
666687
}
667688

668689
/** Computes the default value based on the schema type.
@@ -689,7 +710,7 @@ export function getDefaultBasedOnSchemaType<
689710
return getObjectDefaults(validator, rawSchema, computeDefaultsProps, defaults);
690711
}
691712
case 'array': {
692-
return getArrayDefaults(validator, rawSchema, computeDefaultsProps, defaults);
713+
return getArrayDefaults(validator, rawSchema, computeDefaultsProps, defaults as T[]);
693714
}
694715
}
695716
}

packages/utils/test/schema/getDefaultFormStateTest.ts

Lines changed: 127 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createSchemaUtils, Experimental_DefaultFormStateBehavior, getDefaultFormState, RJSFSchema } from '../../src';
22
import {
33
AdditionalItemsHandling,
4+
computeDefaultBasedOnSchemaTypeAndDefaults,
45
computeDefaults,
56
getArrayDefaults,
67
getDefaultBasedOnSchemaType,
@@ -1969,7 +1970,119 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
19691970
]);
19701971
});
19711972
});
1972-
1973+
describe('computeDefaultBasedOnSchemaTypeAndDefaults()', () => {
1974+
let schema: RJSFSchema;
1975+
describe('Object', () => {
1976+
beforeAll(() => {
1977+
schema = {
1978+
type: 'object',
1979+
default: null,
1980+
};
1981+
});
1982+
it('computedDefaults is undefined', () => {
1983+
expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, undefined)).toBeUndefined();
1984+
});
1985+
it('computedDefaults is empty object', () => {
1986+
expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, {})).toEqual({});
1987+
});
1988+
it('computedDefaults is non-empty object', () => {
1989+
const computedDefault = { foo: 'bar' };
1990+
expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, computedDefault)).toEqual(computedDefault);
1991+
});
1992+
});
1993+
describe('Nullable Object', () => {
1994+
beforeAll(() => {
1995+
schema = {
1996+
type: ['null', 'object'],
1997+
default: null,
1998+
};
1999+
});
2000+
it('computedDefaults is undefined', () => {
2001+
expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, undefined)).toBeNull();
2002+
});
2003+
it('computedDefaults is empty object', () => {
2004+
expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, {})).toBeNull();
2005+
});
2006+
it('computedDefaults is non-empty object', () => {
2007+
const computedDefault = { foo: 'bar' };
2008+
expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, computedDefault)).toEqual(computedDefault);
2009+
});
2010+
});
2011+
describe('Array', () => {
2012+
beforeAll(() => {
2013+
schema = {
2014+
type: 'array',
2015+
default: null,
2016+
items: { type: 'string' },
2017+
};
2018+
});
2019+
it('computedDefaults is undefined', () => {
2020+
expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, undefined)).toBeUndefined();
2021+
});
2022+
it('computedDefaults is empty object', () => {
2023+
expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, [])).toEqual([]);
2024+
});
2025+
it('computedDefaults is non-empty object', () => {
2026+
const computedDefault = ['bar'];
2027+
expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, computedDefault)).toEqual(computedDefault);
2028+
});
2029+
});
2030+
describe('Nullable Array', () => {
2031+
beforeAll(() => {
2032+
schema = {
2033+
type: ['null', 'array'],
2034+
default: null,
2035+
items: { type: 'string' },
2036+
};
2037+
});
2038+
it('computedDefaults is undefined', () => {
2039+
expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, undefined)).toBeNull();
2040+
});
2041+
it('computedDefaults is empty object', () => {
2042+
expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, [])).toBeNull();
2043+
});
2044+
it('computedDefaults is non-empty object', () => {
2045+
const computedDefault = ['bar'];
2046+
expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, computedDefault)).toEqual(computedDefault);
2047+
});
2048+
});
2049+
describe('Nullable String', () => {
2050+
beforeAll(() => {
2051+
schema = {
2052+
type: 'string',
2053+
default: null,
2054+
};
2055+
});
2056+
it('computedDefaults is undefined', () => {
2057+
expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, undefined)).toBeUndefined();
2058+
});
2059+
it('computedDefaults is empty object', () => {
2060+
expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, '')).toEqual('');
2061+
});
2062+
it('computedDefaults is non-empty object', () => {
2063+
const computedDefault = 'bar';
2064+
expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, computedDefault)).toEqual(computedDefault);
2065+
});
2066+
});
2067+
describe('Nullable String', () => {
2068+
beforeAll(() => {
2069+
schema = {
2070+
type: ['null', 'string'],
2071+
default: null,
2072+
};
2073+
});
2074+
it('computedDefaults is undefined', () => {
2075+
expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, undefined)).toBeNull();
2076+
});
2077+
it('computedDefaults is empty object', () => {
2078+
expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, '')).toBeNull();
2079+
});
2080+
it('computedDefaults is non-empty object', () => {
2081+
const computedDefault = 'bar';
2082+
expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, computedDefault)).toEqual(computedDefault);
2083+
});
2084+
});
2085+
});
19732086
describe('getValidFormData', () => {
19742087
let schema: RJSFSchema;
19752088
it('Test schema with non valid formData for enum property', () => {
@@ -5091,12 +5204,12 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
50915204
expect(Array.isArray(result)).toBe(true);
50925205

50935206
// 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]);
5207+
(result![0] as any).field = 'test-value-1';
5208+
(result![1] as any).field = 'test-value-2';
5209+
expect((result![2] as any).field).toBeUndefined();
5210+
expect(result![0]).not.toBe(result![1]);
5211+
expect(result![1]).not.toBe(result![2]);
5212+
expect(result![0]).not.toBe(result![2]);
51005213
});
51015214

51025215
it('should ensure array items with default values are independent instances', () => {
@@ -5125,9 +5238,9 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
51255238
expect(Array.isArray(result)).toBe(true);
51265239

51275240
// 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]);
5241+
(result![0] as any).field = 'modified-value';
5242+
expect((result![1] as any).field).toBe('default-value');
5243+
expect(result![0]).not.toBe(result![1]);
51315244
});
51325245

51335246
it('should ensure nested objects in arrays are independent instances', () => {
@@ -5164,10 +5277,10 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
51645277
expect(Array.isArray(result)).toBe(true);
51655278

51665279
// 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);
5280+
(result![0] as any).nested.value = 'modified-nested-value';
5281+
expect((result![1] as any).nested.value).toBe('nested-default');
5282+
expect(result![0]).not.toBe(result![1]);
5283+
expect((result![0] as any).nested).not.toBe((result![1] as any).nested);
51715284
});
51725285
});
51735286
});

0 commit comments

Comments
 (0)