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 = {