Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ should change the heading of the (upcoming) version to include a major version b

## @rjsf/utils

- Fix for AJV [$data](https://ajv.js.org/guide/combining-schemas.html#data-reference) reference in const property in schema treated as default/const value. The issue is mentioned in [#4361](https://github.com/rjsf-team/react-jsonschema-form/issues/4361).
- Fixed issue with formData not updating when dependencies change, fixing [#4325](https://github.com/rjsf-team/react-jsonschema-form/issues/4325)
- Fixed issue with assigning default values to formData with deeply nested required properties, fixing [#4399](https://github.com/rjsf-team/react-jsonschema-form/issues/4399)

Expand Down
43 changes: 43 additions & 0 deletions packages/core/test/ObjectField.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,49 @@ describe('ObjectField', () => {
);
});

it('should validate AJV $data reference ', () => {
const schema = {
type: 'object',
properties: {
email: {
type: 'string',
title: 'E-mail',
format: 'email',
},
emailConfirm: {
type: 'string',
const: {
$data: '/email',
},
title: 'Confirm e-mail',
format: 'email',
},
},
};
const { node, rerender } = createFormComponent({
schema,
formData: {
email: '[email protected]',
emailConfirm: '[email protected]',
},
liveValidate: true,
});

const errorMessages = node.querySelectorAll('#root_emailConfirm__error');
expect(errorMessages).to.have.length(1);

rerender({
schema,
formData: {
email: '[email protected]',
emailConfirm: '[email protected]',
},
liveValidate: true,
});

expect(node.querySelectorAll('#root_foo__error')).to.have.length(0);
});

it('Check that when formData changes, the form should re-validate', () => {
const { node, rerender } = createFormComponent({
schema,
Expand Down
2 changes: 2 additions & 0 deletions packages/playground/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ const esV8Validator = customizeValidator({}, localize_es);
const AJV8_2019 = customizeValidator({ AjvClass: Ajv2019 });
const AJV8_2020 = customizeValidator({ AjvClass: Ajv2020 });
const AJV8_DISC = customizeValidator({ ajvOptionsOverrides: { discriminator: true } });
const AJV8_DATA_REF = customizeValidator({ ajvOptionsOverrides: { $data: true } });

const validators: PlaygroundProps['validators'] = {
AJV8: v8Validator,
'AJV8 $data reference': AJV8_DATA_REF,
'AJV8 (discriminator)': AJV8_DISC,
AJV8_es: esV8Validator,
AJV8_2019,
Expand Down
3 changes: 2 additions & 1 deletion packages/playground/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ export default function Header({
uiSchema,
theme,
liveSettings,
validator,
})
);

Expand All @@ -314,7 +315,7 @@ export default function Header({
setShareURL(null);
console.error(error);
}
}, [formData, liveSettings, schema, theme, uiSchema, setShareURL]);
}, [formData, liveSettings, schema, theme, uiSchema, validator, setShareURL]);

return (
<div className='page-header'>
Expand Down
2 changes: 2 additions & 0 deletions packages/playground/src/components/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export default function Playground({ themes, validators }: PlaygroundProps) {
theme: dataTheme = theme,
extraErrors,
liveSettings,
validator,
...rest
} = data;

Expand All @@ -85,6 +86,7 @@ export default function Playground({ themes, validators }: PlaygroundProps) {
setTheme(theTheme);
setShowForm(true);
setLiveSettings(liveSettings);
setValidator(validator);
setOtherFormProps({ fields, templates, ...rest });
},
[theme, onThemeSelected, themes]
Expand Down
4 changes: 3 additions & 1 deletion packages/playground/src/samples/Sample.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { FormProps } from '@rjsf/core';

export type Sample = Omit<FormProps, 'validator'>;
export interface Sample extends Omit<FormProps, 'validator'> {
validator: string;
}
17 changes: 17 additions & 0 deletions packages/utils/src/constIsAjvDataReference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { CONST_KEY, getSchemaType, isObject } from './';
import { RJSFSchema, StrictRJSFSchema } from './types';
import { JSONSchema7Type } from 'json-schema';
import isString from 'lodash/isString';

/**
* Checks if the schema const property value is an AJV $data reference
* and the current schema is not an object or array
*
* @param schema - The schema to check if the const is an AJV $data reference
* @returns - true if the schema const property value is an AJV $data reference otherwise false.
*/
export default function constIsAjvDataReference<S extends StrictRJSFSchema = RJSFSchema>(schema: S): boolean {
const schemaConst = schema[CONST_KEY] as JSONSchema7Type & { $data: string };
const schemaType = getSchemaType<S>(schema);
return isObject(schemaConst) && isString(schemaConst?.$data) && schemaType !== 'object' && schemaType !== 'array';
}
12 changes: 9 additions & 3 deletions packages/utils/src/schema/getDefaultFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import isSelect from './isSelect';
import retrieveSchema, { resolveDependencies } from './retrieveSchema';
import isConstant from '../isConstant';
import { JSONSchema7Object } from 'json-schema';
import constIsAjvDataReference from '../constIsAjvDataReference';
import isEqual from 'lodash/isEqual';
import optionsList from '../optionsList';

Expand Down Expand Up @@ -213,8 +214,12 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
let experimental_dfsb_to_compute = experimental_defaultFormStateBehavior;
let updatedRecurseList = _recurseList;

if (schema[CONST_KEY] && experimental_defaultFormStateBehavior?.constAsDefaults !== 'never') {
defaults = schema.const as unknown as T;
if (
schema[CONST_KEY] &&
experimental_defaultFormStateBehavior?.constAsDefaults !== 'never' &&
!constIsAjvDataReference(schema)
) {
defaults = schema[CONST_KEY] as unknown as T;
} else if (isObject(defaults) && isObject(schema.default)) {
// For object defaults, only override parent defaults that are defined in
// schema.default.
Expand Down Expand Up @@ -431,7 +436,8 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
const hasParentConst = isObject(parentConst) && (parentConst as JSONSchema7Object)[key] !== undefined;
const hasConst =
((isObject(propertySchema) && CONST_KEY in propertySchema) || hasParentConst) &&
experimental_defaultFormStateBehavior?.constAsDefaults !== 'never';
experimental_defaultFormStateBehavior?.constAsDefaults !== 'never' &&
!constIsAjvDataReference(propertySchema);
// Compute the defaults for this node, with the parent defaults we might
// have from a previous run: defaults[key].
const computedDefault = computeDefaults<T, S, F>(validator, propertySchema, {
Expand Down
53 changes: 53 additions & 0 deletions packages/utils/test/constIsAjvDataReference.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { RJSFSchema } from 'src';
import constIsAjvDataReference from '../src/constIsAjvDataReference';

describe('constIsAjvDataReference()', () => {
describe('check if schema contains $data reference', () => {
it('should return true when the const property contains a $data reference', () => {
const schema: RJSFSchema = {
type: 'string',
const: {
$data: '/email',
},
title: 'Confirm e-mail',
format: 'email',
};
expect(constIsAjvDataReference(schema)).toEqual(true);
});

it('should return false when the const property does not contain a $data reference', () => {
const schema: RJSFSchema = {
type: 'string',
const: 'hello world',
};
expect(constIsAjvDataReference(schema)).toEqual(false);
});

it('Should return false when the const property is not present in the schema', () => {
const schema: RJSFSchema = {
type: 'string',
};
expect(constIsAjvDataReference(schema)).toEqual(false);
});

it('Should return false when the $data reference is at the object level.', () => {
const schema: RJSFSchema = {
type: 'object',
properties: {
$data: {
type: 'string',
},
},
const: {
$data: 'Hello World!',
},
};
expect(constIsAjvDataReference(schema)).toEqual(false);
});

it('should return false when the schema is invalid', () => {
const schema = 'hello world' as unknown as RJSFSchema;
expect(constIsAjvDataReference(schema)).toEqual(false);
});
});
});
96 changes: 96 additions & 0 deletions packages/utils/test/schema/getDefaultFormStateTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2113,6 +2113,102 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
expect(ensureFormDataMatchingSchema(testValidator, schema, schema, 'a')).toEqual('a');
});
});
describe('AJV $data reference in const property in schema should not be treated as default/const value', () => {
let schema: RJSFSchema;
it('test nested object with $data in the schema', () => {
schema = {
type: 'object',
properties: {
email: {
type: 'string',
title: 'E-mail',
format: 'email',
},
emailConfirm: {
type: 'string',
const: {
$data: '/email',
},
title: 'Confirm e-mail',
format: 'email',
},
nestedObject: {
type: 'object',
properties: {
nestedEmail: {
type: 'string',
title: 'E-mail',
format: 'email',
},
nestedEmailConfirm: {
type: 'string',
title: 'Confirm e-mail',
const: {
$data: '/nestedObject/nestedEmail',
},
format: 'email',
},
},
},
nestedObjectConfirm: {
type: 'object',
properties: {
nestedEmailConfirm: {
type: 'string',
title: 'Confirm e-mail',
const: {
$data: '/nestedObject/nestedEmail',
},
format: 'email',
},
},
},
arrayConfirm: {
type: 'array',
items: {
type: 'string',
title: 'Confirm e-mail',
const: {
$data: '/nestedObject/nestedEmail',
},
format: 'email',
},
},
},
};
expect(
computeDefaults(testValidator, schema, {
rootSchema: schema,
})
).toEqual({
arrayConfirm: [],
});
});
it('test nested object with $data in the schema and emptyObjectFields set to populateRequiredDefaults', () => {
expect(
computeDefaults(testValidator, schema, {
rootSchema: schema,
experimental_defaultFormStateBehavior: { emptyObjectFields: 'populateRequiredDefaults' },
})
).toEqual({});
});
it('test nested object with $data in the schema and emptyObjectFields set to skipEmptyDefaults', () => {
expect(
computeDefaults(testValidator, schema, {
rootSchema: schema,
experimental_defaultFormStateBehavior: { emptyObjectFields: 'skipEmptyDefaults' },
})
).toEqual({});
});
it('test nested object with $data in the schema and emptyObjectFields set to skipDefaults', () => {
expect(
computeDefaults(testValidator, schema, {
rootSchema: schema,
experimental_defaultFormStateBehavior: { emptyObjectFields: 'skipDefaults' },
})
).toEqual({});
});
});
describe('default form state behavior: ignore min items unless required', () => {
it('should return empty data for an optional array property with minItems', () => {
const schema: RJSFSchema = {
Expand Down
Loading