diff --git a/CHANGELOG.md b/CHANGELOG.md index 62d7ba6cb4..aecd576993 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ should change the heading of the (upcoming) version to include a major version b - 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) +- 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). # 5.23.2 diff --git a/packages/core/test/ObjectField.test.jsx b/packages/core/test/ObjectField.test.jsx index 1dd8f88a85..5bc7ec8a5d 100644 --- a/packages/core/test/ObjectField.test.jsx +++ b/packages/core/test/ObjectField.test.jsx @@ -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: 'Appie@hotmail.com', + emailConfirm: 'wrong@wrong.com', + }, + liveValidate: true, + }); + + const errorMessages = node.querySelectorAll('#root_emailConfirm__error'); + expect(errorMessages).to.have.length(1); + + rerender({ + schema, + formData: { + email: 'Appie@hotmail.com', + emailConfirm: 'Appie@hotmail.com', + }, + 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, diff --git a/packages/playground/src/app.tsx b/packages/playground/src/app.tsx index 110eef97d6..0997ac50d5 100644 --- a/packages/playground/src/app.tsx +++ b/packages/playground/src/app.tsx @@ -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, diff --git a/packages/playground/src/components/Header.tsx b/packages/playground/src/components/Header.tsx index 873666c6e3..16ca4d2595 100644 --- a/packages/playground/src/components/Header.tsx +++ b/packages/playground/src/components/Header.tsx @@ -306,6 +306,7 @@ export default function Header({ uiSchema, theme, liveSettings, + validator, }) ); @@ -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 (
diff --git a/packages/playground/src/components/Playground.tsx b/packages/playground/src/components/Playground.tsx index 0dacfbbf78..ca285bfbff 100644 --- a/packages/playground/src/components/Playground.tsx +++ b/packages/playground/src/components/Playground.tsx @@ -69,6 +69,7 @@ export default function Playground({ themes, validators }: PlaygroundProps) { theme: dataTheme = theme, extraErrors, liveSettings, + validator, ...rest } = data; @@ -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] diff --git a/packages/playground/src/samples/Sample.ts b/packages/playground/src/samples/Sample.ts index 5c4d5609f8..5855d22019 100644 --- a/packages/playground/src/samples/Sample.ts +++ b/packages/playground/src/samples/Sample.ts @@ -1,3 +1,5 @@ import { FormProps } from '@rjsf/core'; -export type Sample = Omit; +export interface Sample extends Omit { + validator: string; +} diff --git a/packages/utils/src/constIsAjvDataReference.ts b/packages/utils/src/constIsAjvDataReference.ts new file mode 100644 index 0000000000..293649a949 --- /dev/null +++ b/packages/utils/src/constIsAjvDataReference.ts @@ -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(schema: S): boolean { + const schemaConst = schema[CONST_KEY] as JSONSchema7Type & { $data: string }; + const schemaType = getSchemaType(schema); + return isObject(schemaConst) && isString(schemaConst?.$data) && schemaType !== 'object' && schemaType !== 'array'; +} diff --git a/packages/utils/src/schema/getDefaultFormState.ts b/packages/utils/src/schema/getDefaultFormState.ts index 0ac0ca6da3..7974e1b07d 100644 --- a/packages/utils/src/schema/getDefaultFormState.ts +++ b/packages/utils/src/schema/getDefaultFormState.ts @@ -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'; @@ -213,8 +214,12 @@ export function computeDefaults(validator, propertySchema, { diff --git a/packages/utils/test/constIsAjvDataReference.test.ts b/packages/utils/test/constIsAjvDataReference.test.ts new file mode 100644 index 0000000000..cc597dd659 --- /dev/null +++ b/packages/utils/test/constIsAjvDataReference.test.ts @@ -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); + }); + }); +}); diff --git a/packages/utils/test/schema/getDefaultFormStateTest.ts b/packages/utils/test/schema/getDefaultFormStateTest.ts index f546dbd88b..6a75006d92 100644 --- a/packages/utils/test/schema/getDefaultFormStateTest.ts +++ b/packages/utils/test/schema/getDefaultFormStateTest.ts @@ -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 = {