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

-->

# 5.24.0

- 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

## @rjsf/core
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 @@ -227,6 +227,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 @@ -32,6 +32,7 @@ import {
import isMultiSelect from './isMultiSelect';
import retrieveSchema, { resolveDependencies } from './retrieveSchema';
import { JSONSchema7Object } from 'json-schema';
import constIsAjvDataReference from '../constIsAjvDataReference';

const PRIMITIVE_TYPES = ['string', 'number', 'integer', 'boolean', 'null'];

Expand Down Expand Up @@ -203,8 +204,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 @@ -357,7 +362,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
96 changes: 96 additions & 0 deletions packages/utils/test/schema/getDefaultFormStateTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1750,6 +1750,102 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
expect(getArrayDefaults(testValidator, schema)).toStrictEqual([]);
});
});
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