Skip to content

Commit 69b450b

Browse files
fix: apply nested const defaults in conditional schemas (#5132)
Co-authored-by: Heath C <51679588+heath-freenome@users.noreply.github.com>
1 parent 52180fa commit 69b450b

3 files changed

Lines changed: 122 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ should change the heading of the (upcoming) version to include a major version b
3636
- Added a warning when a `select` widget schema default is not present in the enum options, fixing [#4494](https://github.com/rjsf-team/react-jsonschema-form/issues/4494)
3737
- Fixed `SchemaField` so that a schema with a primitive or array `type` alongside a non-select `oneOf`/`anyOf` no longer renders a spurious duplicate input above the option selector, fixing [#5119](https://github.com/rjsf-team/react-jsonschema-form/issues/5119)
3838
- Fixed `MultiSchemaField` to propagate the parent schema's `type` to option sub-schemas that don't define their own, so the correct widget (e.g. `StringField`) renders for the selected option instead of `FallbackField`, fixing [#5119](https://github.com/rjsf-team/react-jsonschema-form/issues/5119)
39+
- Fixed nested `constAsDefaults` values being skipped in matching conditional `allOf` schemas, fixing [#4963](https://github.com/rjsf-team/react-jsonschema-form/issues/4963)
3940

4041
## @rjsf/daisyui
4142

packages/utils/src/schema/getDefaultFormState.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -460,19 +460,44 @@ export function ensureFormDataMatchingSchema<
460460
experimental_defaultFormStateBehavior?: Experimental_DefaultFormStateBehavior,
461461
experimental_customMergeAllOf?: Experimental_CustomMergeAllOf<S>,
462462
): T | T[] | undefined {
463+
const shouldRetrieveAllOf =
464+
experimental_defaultFormStateBehavior?.allOf === 'populateDefaults' && ALL_OF_KEY in schema;
465+
const schemaToMatch = shouldRetrieveAllOf
466+
? retrieveSchema<T, S, F>(validator, schema, rootSchema, formData, experimental_customMergeAllOf)
467+
: schema;
463468
const isSelectField =
464-
!isConstant<S>(schema) && isSelect<T, S, F>(validator, schema, rootSchema, experimental_customMergeAllOf);
469+
!isConstant<S>(schemaToMatch) &&
470+
isSelect<T, S, F>(validator, schemaToMatch, rootSchema, experimental_customMergeAllOf);
465471
let validFormData: T | T[] | undefined = formData;
466472
if (isSelectField) {
467-
const getOptionsList = optionsList<T, S, F>(schema);
473+
const getOptionsList = optionsList<T, S, F>(schemaToMatch);
468474
const isValid = getOptionsList?.some((option) => deepEquals(option.value, formData));
469475
validFormData = isValid ? formData : undefined;
470476
}
471477

472478
// Override the formData with the const if the constAsDefaults is set to always
473-
const constTakesPrecedence = schema[CONST_KEY] && experimental_defaultFormStateBehavior?.constAsDefaults === 'always';
479+
const constTakesPrecedence =
480+
schemaToMatch[CONST_KEY] && experimental_defaultFormStateBehavior?.constAsDefaults === 'always';
474481
if (constTakesPrecedence) {
475-
validFormData = schema.const as T;
482+
validFormData = schemaToMatch.const as T;
483+
} else if (isObject(validFormData) && isObject(schemaToMatch.properties)) {
484+
validFormData = Object.keys(schemaToMatch.properties).reduce(
485+
(acc: GenericObjectType, key: string) => {
486+
const propertySchema: S = get(schemaToMatch, [PROPERTIES_KEY, key], {}) as S;
487+
if (key in acc && (shouldRetrieveAllOf || (isObject(propertySchema) && ALL_OF_KEY in propertySchema))) {
488+
acc[key] = ensureFormDataMatchingSchema<T, S, F>(
489+
validator,
490+
propertySchema,
491+
rootSchema,
492+
get(acc, key),
493+
experimental_defaultFormStateBehavior,
494+
experimental_customMergeAllOf,
495+
);
496+
}
497+
return acc;
498+
},
499+
{ ...(validFormData as GenericObjectType) },
500+
) as T;
476501
}
477502

478503
return validFormData;
@@ -825,9 +850,17 @@ export default function getDefaultFormState<
825850
if (isObject(formData) || Array.isArray(formData)) {
826851
const { mergeDefaultsIntoFormData } = experimental_defaultFormStateBehavior || {};
827852
const defaultSupercedesUndefined = mergeDefaultsIntoFormData === 'useDefaultIfFormDataUndefined';
853+
const matchingFormData = ensureFormDataMatchingSchema<T, S, F>(
854+
validator,
855+
schema,
856+
rootSchema ?? schema,
857+
formData,
858+
experimental_defaultFormStateBehavior,
859+
experimental_customMergeAllOf,
860+
);
828861
const result = mergeDefaultsWithFormData<T | T[]>(
829862
defaults,
830-
formData,
863+
matchingFormData,
831864
true, // set to true to add any additional default array entries.
832865
defaultSupercedesUndefined,
833866
true, // set to true to override formData with defaults if they exist.

packages/utils/test/schema/getDefaultFormStateTest.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5327,6 +5327,89 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
53275327
}),
53285328
).toEqual({ animalInfo: { animal: 'Cat', food: 'meat' } });
53295329
});
5330+
5331+
it('should apply conditional const defaults consistently for root and nested allOf schemas', () => {
5332+
const experimental_defaultFormStateBehavior: Experimental_DefaultFormStateBehavior = {
5333+
allOf: 'populateDefaults',
5334+
constAsDefaults: 'always',
5335+
};
5336+
const conditionalSchema = {
5337+
type: 'object',
5338+
properties: {
5339+
animal: {
5340+
enum: ['Cat', 'Fish'],
5341+
},
5342+
food: {
5343+
type: 'string',
5344+
enum: ['meat', 'grass', 'fish'],
5345+
},
5346+
},
5347+
allOf: [
5348+
{
5349+
if: {
5350+
properties: {
5351+
animal: {
5352+
const: 'Cat',
5353+
},
5354+
},
5355+
},
5356+
then: {
5357+
properties: {
5358+
food: {
5359+
const: 'fish',
5360+
},
5361+
},
5362+
required: ['food'],
5363+
},
5364+
},
5365+
{
5366+
required: ['animal'],
5367+
},
5368+
],
5369+
} satisfies RJSFSchema;
5370+
const formData = {
5371+
animal: 'Cat',
5372+
food: 'meat',
5373+
};
5374+
5375+
expect(
5376+
getDefaultFormState(
5377+
testValidator,
5378+
conditionalSchema,
5379+
formData,
5380+
conditionalSchema,
5381+
undefined,
5382+
experimental_defaultFormStateBehavior,
5383+
),
5384+
).toEqual({
5385+
animal: 'Cat',
5386+
food: 'fish',
5387+
});
5388+
5389+
const nestedSchema: RJSFSchema = {
5390+
type: 'object',
5391+
properties: {
5392+
nested: conditionalSchema,
5393+
},
5394+
required: ['nested'],
5395+
};
5396+
5397+
expect(
5398+
getDefaultFormState(
5399+
testValidator,
5400+
nestedSchema,
5401+
{ nested: formData },
5402+
nestedSchema,
5403+
undefined,
5404+
experimental_defaultFormStateBehavior,
5405+
),
5406+
).toEqual({
5407+
nested: {
5408+
animal: 'Cat',
5409+
food: 'fish',
5410+
},
5411+
});
5412+
});
53305413
});
53315414

53325415
describe('default form state behaviour: allOf = "skipDefaults"', () => {

0 commit comments

Comments
 (0)