Skip to content

Commit a521990

Browse files
Bug: AJV $data reference in const property in schema treated as default/const value. (#4431)
* Validator to support AJV $data reference and saving validator state in ShareURL. * Fix for AJV $data reference in const property in schema treated as default/const value. * written a test for consIsAjvDataReference. * Update CHANGELOG.md Moving the change to the end of the existing list --------- Co-authored-by: Heath C <[email protected]>
1 parent f5a24b2 commit a521990

File tree

10 files changed

+228
-5
lines changed

10 files changed

+228
-5
lines changed

CHANGELOG.md

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

2727
- Fixed issue with formData not updating when dependencies change, fixing [#4325](https://github.com/rjsf-team/react-jsonschema-form/issues/4325)
2828
- 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)
29+
- 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).
2930

3031
# 5.23.2
3132

packages/core/test/ObjectField.test.jsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,49 @@ describe('ObjectField', () => {
277277
);
278278
});
279279

280+
it('should validate AJV $data reference ', () => {
281+
const schema = {
282+
type: 'object',
283+
properties: {
284+
email: {
285+
type: 'string',
286+
title: 'E-mail',
287+
format: 'email',
288+
},
289+
emailConfirm: {
290+
type: 'string',
291+
const: {
292+
$data: '/email',
293+
},
294+
title: 'Confirm e-mail',
295+
format: 'email',
296+
},
297+
},
298+
};
299+
const { node, rerender } = createFormComponent({
300+
schema,
301+
formData: {
302+
303+
emailConfirm: '[email protected]',
304+
},
305+
liveValidate: true,
306+
});
307+
308+
const errorMessages = node.querySelectorAll('#root_emailConfirm__error');
309+
expect(errorMessages).to.have.length(1);
310+
311+
rerender({
312+
schema,
313+
formData: {
314+
315+
emailConfirm: '[email protected]',
316+
},
317+
liveValidate: true,
318+
});
319+
320+
expect(node.querySelectorAll('#root_foo__error')).to.have.length(0);
321+
});
322+
280323
it('Check that when formData changes, the form should re-validate', () => {
281324
const { node, rerender } = createFormComponent({
282325
schema,

packages/playground/src/app.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ const esV8Validator = customizeValidator({}, localize_es);
2020
const AJV8_2019 = customizeValidator({ AjvClass: Ajv2019 });
2121
const AJV8_2020 = customizeValidator({ AjvClass: Ajv2020 });
2222
const AJV8_DISC = customizeValidator({ ajvOptionsOverrides: { discriminator: true } });
23+
const AJV8_DATA_REF = customizeValidator({ ajvOptionsOverrides: { $data: true } });
2324

2425
const validators: PlaygroundProps['validators'] = {
2526
AJV8: v8Validator,
27+
'AJV8 $data reference': AJV8_DATA_REF,
2628
'AJV8 (discriminator)': AJV8_DISC,
2729
AJV8_es: esV8Validator,
2830
AJV8_2019,

packages/playground/src/components/Header.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ export default function Header({
306306
uiSchema,
307307
theme,
308308
liveSettings,
309+
validator,
309310
})
310311
);
311312

@@ -314,7 +315,7 @@ export default function Header({
314315
setShareURL(null);
315316
console.error(error);
316317
}
317-
}, [formData, liveSettings, schema, theme, uiSchema, setShareURL]);
318+
}, [formData, liveSettings, schema, theme, uiSchema, validator, setShareURL]);
318319

319320
return (
320321
<div className='page-header'>

packages/playground/src/components/Playground.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export default function Playground({ themes, validators }: PlaygroundProps) {
6969
theme: dataTheme = theme,
7070
extraErrors,
7171
liveSettings,
72+
validator,
7273
...rest
7374
} = data;
7475

@@ -85,6 +86,7 @@ export default function Playground({ themes, validators }: PlaygroundProps) {
8586
setTheme(theTheme);
8687
setShowForm(true);
8788
setLiveSettings(liveSettings);
89+
setValidator(validator);
8890
setOtherFormProps({ fields, templates, ...rest });
8991
},
9092
[theme, onThemeSelected, themes]
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
import { FormProps } from '@rjsf/core';
22

3-
export type Sample = Omit<FormProps, 'validator'>;
3+
export interface Sample extends Omit<FormProps, 'validator'> {
4+
validator: string;
5+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { CONST_KEY, getSchemaType, isObject } from './';
2+
import { RJSFSchema, StrictRJSFSchema } from './types';
3+
import { JSONSchema7Type } from 'json-schema';
4+
import isString from 'lodash/isString';
5+
6+
/**
7+
* Checks if the schema const property value is an AJV $data reference
8+
* and the current schema is not an object or array
9+
*
10+
* @param schema - The schema to check if the const is an AJV $data reference
11+
* @returns - true if the schema const property value is an AJV $data reference otherwise false.
12+
*/
13+
export default function constIsAjvDataReference<S extends StrictRJSFSchema = RJSFSchema>(schema: S): boolean {
14+
const schemaConst = schema[CONST_KEY] as JSONSchema7Type & { $data: string };
15+
const schemaType = getSchemaType<S>(schema);
16+
return isObject(schemaConst) && isString(schemaConst?.$data) && schemaType !== 'object' && schemaType !== 'array';
17+
}

packages/utils/src/schema/getDefaultFormState.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import isSelect from './isSelect';
3434
import retrieveSchema, { resolveDependencies } from './retrieveSchema';
3535
import isConstant from '../isConstant';
3636
import { JSONSchema7Object } from 'json-schema';
37+
import constIsAjvDataReference from '../constIsAjvDataReference';
3738
import isEqual from 'lodash/isEqual';
3839
import optionsList from '../optionsList';
3940

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

216-
if (schema[CONST_KEY] && experimental_defaultFormStateBehavior?.constAsDefaults !== 'never') {
217-
defaults = schema.const as unknown as T;
217+
if (
218+
schema[CONST_KEY] &&
219+
experimental_defaultFormStateBehavior?.constAsDefaults !== 'never' &&
220+
!constIsAjvDataReference(schema)
221+
) {
222+
defaults = schema[CONST_KEY] as unknown as T;
218223
} else if (isObject(defaults) && isObject(schema.default)) {
219224
// For object defaults, only override parent defaults that are defined in
220225
// schema.default.
@@ -431,7 +436,8 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
431436
const hasParentConst = isObject(parentConst) && (parentConst as JSONSchema7Object)[key] !== undefined;
432437
const hasConst =
433438
((isObject(propertySchema) && CONST_KEY in propertySchema) || hasParentConst) &&
434-
experimental_defaultFormStateBehavior?.constAsDefaults !== 'never';
439+
experimental_defaultFormStateBehavior?.constAsDefaults !== 'never' &&
440+
!constIsAjvDataReference(propertySchema);
435441
// Compute the defaults for this node, with the parent defaults we might
436442
// have from a previous run: defaults[key].
437443
const computedDefault = computeDefaults<T, S, F>(validator, propertySchema, {
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { RJSFSchema } from 'src';
2+
import constIsAjvDataReference from '../src/constIsAjvDataReference';
3+
4+
describe('constIsAjvDataReference()', () => {
5+
describe('check if schema contains $data reference', () => {
6+
it('should return true when the const property contains a $data reference', () => {
7+
const schema: RJSFSchema = {
8+
type: 'string',
9+
const: {
10+
$data: '/email',
11+
},
12+
title: 'Confirm e-mail',
13+
format: 'email',
14+
};
15+
expect(constIsAjvDataReference(schema)).toEqual(true);
16+
});
17+
18+
it('should return false when the const property does not contain a $data reference', () => {
19+
const schema: RJSFSchema = {
20+
type: 'string',
21+
const: 'hello world',
22+
};
23+
expect(constIsAjvDataReference(schema)).toEqual(false);
24+
});
25+
26+
it('Should return false when the const property is not present in the schema', () => {
27+
const schema: RJSFSchema = {
28+
type: 'string',
29+
};
30+
expect(constIsAjvDataReference(schema)).toEqual(false);
31+
});
32+
33+
it('Should return false when the $data reference is at the object level.', () => {
34+
const schema: RJSFSchema = {
35+
type: 'object',
36+
properties: {
37+
$data: {
38+
type: 'string',
39+
},
40+
},
41+
const: {
42+
$data: 'Hello World!',
43+
},
44+
};
45+
expect(constIsAjvDataReference(schema)).toEqual(false);
46+
});
47+
48+
it('should return false when the schema is invalid', () => {
49+
const schema = 'hello world' as unknown as RJSFSchema;
50+
expect(constIsAjvDataReference(schema)).toEqual(false);
51+
});
52+
});
53+
});

packages/utils/test/schema/getDefaultFormStateTest.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2113,6 +2113,102 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
21132113
expect(ensureFormDataMatchingSchema(testValidator, schema, schema, 'a')).toEqual('a');
21142114
});
21152115
});
2116+
describe('AJV $data reference in const property in schema should not be treated as default/const value', () => {
2117+
let schema: RJSFSchema;
2118+
it('test nested object with $data in the schema', () => {
2119+
schema = {
2120+
type: 'object',
2121+
properties: {
2122+
email: {
2123+
type: 'string',
2124+
title: 'E-mail',
2125+
format: 'email',
2126+
},
2127+
emailConfirm: {
2128+
type: 'string',
2129+
const: {
2130+
$data: '/email',
2131+
},
2132+
title: 'Confirm e-mail',
2133+
format: 'email',
2134+
},
2135+
nestedObject: {
2136+
type: 'object',
2137+
properties: {
2138+
nestedEmail: {
2139+
type: 'string',
2140+
title: 'E-mail',
2141+
format: 'email',
2142+
},
2143+
nestedEmailConfirm: {
2144+
type: 'string',
2145+
title: 'Confirm e-mail',
2146+
const: {
2147+
$data: '/nestedObject/nestedEmail',
2148+
},
2149+
format: 'email',
2150+
},
2151+
},
2152+
},
2153+
nestedObjectConfirm: {
2154+
type: 'object',
2155+
properties: {
2156+
nestedEmailConfirm: {
2157+
type: 'string',
2158+
title: 'Confirm e-mail',
2159+
const: {
2160+
$data: '/nestedObject/nestedEmail',
2161+
},
2162+
format: 'email',
2163+
},
2164+
},
2165+
},
2166+
arrayConfirm: {
2167+
type: 'array',
2168+
items: {
2169+
type: 'string',
2170+
title: 'Confirm e-mail',
2171+
const: {
2172+
$data: '/nestedObject/nestedEmail',
2173+
},
2174+
format: 'email',
2175+
},
2176+
},
2177+
},
2178+
};
2179+
expect(
2180+
computeDefaults(testValidator, schema, {
2181+
rootSchema: schema,
2182+
})
2183+
).toEqual({
2184+
arrayConfirm: [],
2185+
});
2186+
});
2187+
it('test nested object with $data in the schema and emptyObjectFields set to populateRequiredDefaults', () => {
2188+
expect(
2189+
computeDefaults(testValidator, schema, {
2190+
rootSchema: schema,
2191+
experimental_defaultFormStateBehavior: { emptyObjectFields: 'populateRequiredDefaults' },
2192+
})
2193+
).toEqual({});
2194+
});
2195+
it('test nested object with $data in the schema and emptyObjectFields set to skipEmptyDefaults', () => {
2196+
expect(
2197+
computeDefaults(testValidator, schema, {
2198+
rootSchema: schema,
2199+
experimental_defaultFormStateBehavior: { emptyObjectFields: 'skipEmptyDefaults' },
2200+
})
2201+
).toEqual({});
2202+
});
2203+
it('test nested object with $data in the schema and emptyObjectFields set to skipDefaults', () => {
2204+
expect(
2205+
computeDefaults(testValidator, schema, {
2206+
rootSchema: schema,
2207+
experimental_defaultFormStateBehavior: { emptyObjectFields: 'skipDefaults' },
2208+
})
2209+
).toEqual({});
2210+
});
2211+
});
21162212
describe('default form state behavior: ignore min items unless required', () => {
21172213
it('should return empty data for an optional array property with minItems', () => {
21182214
const schema: RJSFSchema = {

0 commit comments

Comments
 (0)