Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Partially fixed dependency error messages #4417

Merged
merged 5 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -30,6 +30,10 @@ should change the heading of the (upcoming) version to include a major version b
- Fixed issue error message will not be cleared after the controlled Form formData is changed. Fixes [#4426](https://github.com/rjsf-team/react-jsonschema-form/issues/4426)
- 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).

## @rjsf/validator-ajv8

- Partially fixed issue where dependency errors do not show `title` or `ui:title`. This fix only applicable if we use an ajv-i18n localizer. Ref. [#4402](https://github.com/rjsf-team/react-jsonschema-form/issues/4402).

# 5.23.2

## @rjsf/core
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/validator-ajv8/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@types/jest": "^29.5.12",
"@types/json-schema": "^7.0.15",
"@types/lodash": "^4.14.202",
"ajv-i18n": "^4.2.0",
"babel-jest": "^29.7.0",
"eslint": "^8.56.0",
"jest": "^29.7.0",
Expand Down
55 changes: 33 additions & 22 deletions packages/validator-ajv8/src/processRawValidationErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,29 +36,35 @@ export function transformRJSFValidationErrors<
let { message = '' } = rest;
let property = instancePath.replace(/\//g, '.');
let stack = `${property} ${message}`.trim();

if ('missingProperty' in params) {
property = property ? `${property}.${params.missingProperty}` : params.missingProperty;
const currentProperty: string = params.missingProperty;
let uiSchemaTitle = getUiOptions(get(uiSchema, `${property.replace(/^\./, '')}`)).title;
if (uiSchemaTitle === undefined) {
const uiSchemaPath = schemaPath
.replace(/\/properties\//g, '/')
.split('/')
.slice(1, -1)
.concat([currentProperty]);
uiSchemaTitle = getUiOptions(get(uiSchema, uiSchemaPath)).title;
}

if (uiSchemaTitle) {
message = message.replace(`'${currentProperty}'`, `'${uiSchemaTitle}'`);
} else {
const parentSchemaTitle = get(parentSchema, [PROPERTIES_KEY, currentProperty, 'title']);

if (parentSchemaTitle) {
message = message.replace(`'${currentProperty}'`, `'${parentSchemaTitle}'`);
const rawPropertyNames: string[] = [
...(params.deps?.split(', ') || []),
params.missingProperty,
params.property,
].filter((item) => item);

if (rawPropertyNames.length > 0) {
rawPropertyNames.forEach((currentProperty) => {
const path = property ? `${property}.${currentProperty}` : currentProperty;
let uiSchemaTitle = getUiOptions(get(uiSchema, `${path.replace(/^\./, '')}`)).title;
if (uiSchemaTitle === undefined) {
// To retrieve a title from UI schema, construct a path to UI schema from `schemaPath` and `currentProperty`.
// For example, when `#/properties/A/properties/B/required` and `C` are given, they are converted into `['A', 'B', 'C']`.
const uiSchemaPath = schemaPath
.replace(/\/properties\//g, '/')
.split('/')
.slice(1, -1)
.concat([currentProperty]);
Comment on lines +52 to +56
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you provide a comment that simply describes what these chained statements are accomplishing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. I added a comment as follows:

To retrieve a title from UI schema, construct a path to UI schema from `schemaPath` and `currentProperty`.
For example, when `#/properties/A/properties/B/required` and `C` are given, they are converted into `['A', 'B', 'C']`.

uiSchemaTitle = getUiOptions(get(uiSchema, uiSchemaPath)).title;
}
}
if (uiSchemaTitle) {
message = message.replace(`'${currentProperty}'`, `'${uiSchemaTitle}'`);
} else {
const parentSchemaTitle = get(parentSchema, [PROPERTIES_KEY, currentProperty, 'title']);
if (parentSchemaTitle) {
message = message.replace(`'${currentProperty}'`, `'${parentSchemaTitle}'`);
}
}
});

stack = message;
} else {
Expand All @@ -75,6 +81,11 @@ export function transformRJSFValidationErrors<
}
}

// If params.missingProperty is undefined, it is removed from rawPropertyNames by filter((item) => item).
if ('missingProperty' in params) {
heath-freenome marked this conversation as resolved.
Show resolved Hide resolved
property = property ? `${property}.${params.missingProperty}` : params.missingProperty;
}

Comment on lines +85 to +88
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still needed since you are always adding missingProperty to rawPropertyNames above?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need this if clause as params.missingProperty may be undefined. If params.missingProperty is undefined, it is removed from rawPropertyNames by filter((item) => item).

// put data in expected format
return {
name: keyword,
Expand Down
31 changes: 25 additions & 6 deletions packages/validator-ajv8/src/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,19 +90,38 @@ export default class AJV8Validator<T = any, S extends StrictRJSFSchema = RJSFSch
let errors;
if (compiledValidator) {
if (typeof this.localizer === 'function') {
// Missing properties need to be enclosed with quotes so that
// Properties need to be enclosed with quotes so that
// `AJV8Validator#transformRJSFValidationErrors` replaces property names
// with `title` or `ui:title`. See #4348, #4349, and #4387.
// with `title` or `ui:title`. See #4348, #4349, #4387, and #4402.
(compiledValidator.errors ?? []).forEach((error) => {
if (error.params?.missingProperty) {
error.params.missingProperty = `'${error.params.missingProperty}'`;
['missingProperty', 'property'].forEach((key) => {
if (error.params?.[key]) {
error.params[key] = `'${error.params[key]}'`;
}
});
if (error.params?.deps) {
// As `error.params.deps` is the comma+space separated list of missing dependencies, enclose each dependency separately.
// For example, `A, B` is converted into `'A', 'B'`.
error.params.deps = error.params.deps
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you provide a comment that simply describes what these chained statements are accomplishing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a comment as follows:

As `error.params.deps` is the comma+space separated list of missing dependencies, enclose each dependency separately.
For example, `A, B` is converted into `'A', 'B'`.

.split(', ')
.map((v: string) => `'${v}'`)
.join(', ');
}
});
this.localizer(compiledValidator.errors);
// Revert to originals
(compiledValidator.errors ?? []).forEach((error) => {
if (error.params?.missingProperty) {
error.params.missingProperty = error.params.missingProperty.slice(1, -1);
['missingProperty', 'property'].forEach((key) => {
if (error.params?.[key]) {
error.params[key] = error.params[key].slice(1, -1);
}
});
if (error.params?.deps) {
// Remove surrounding quotes from each missing dependency. For example, `'A', 'B'` is reverted to `A, B`.
error.params.deps = error.params.deps
.split(', ')
.map((v: string) => v.slice(1, -1))
.join(', ');
Comment on lines +121 to +124
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you provide a comment that simply describes what these chained statements are accomplishing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a comment as follows:

Remove surrounding quotes from each missing dependency. For example, `'A', 'B'` is reverted to `A, B`.

}
});
}
Expand Down
235 changes: 235 additions & 0 deletions packages/validator-ajv8/test/validator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
UiSchema,
ValidatorType,
} from '@rjsf/utils';
import localize from 'ajv-i18n';

import AJV8Validator from '../src/validator';
import { Localizer } from '../src';
Expand Down Expand Up @@ -2252,6 +2253,240 @@ describe('AJV8Validator', () => {
});
});
});
describe('validating dependencies', () => {
beforeAll(() => {
validator = new AJV8Validator({ AjvClass: Ajv2019 }, localize.en as Localizer);
});
it('should return an error when a dependent is missing', () => {
schema = {
type: 'object',
properties: {
creditCard: {
type: 'number',
},
billingAddress: {
type: 'string',
},
},
dependentRequired: {
creditCard: ['billingAddress'],
},
};
const errors = validator.validateFormData({ creditCard: 1234567890 }, schema);
const errMessage = "must have property 'billingAddress' when property 'creditCard' is present";
expect(errors.errors[0].message).toEqual(errMessage);
expect(errors.errors[0].stack).toEqual(errMessage);
expect(errors.errorSchema).toEqual({
billingAddress: {
__errors: [errMessage],
},
});
expect(errors.errors[0].params.deps).toEqual('billingAddress');
});
it('should return an error when multiple dependents are missing', () => {
schema = {
type: 'object',
properties: {
creditCard: {
type: 'number',
},
holderName: {
type: 'string',
},
billingAddress: {
type: 'string',
},
},
dependentRequired: {
creditCard: ['holderName', 'billingAddress'],
},
};
const errors = validator.validateFormData({ creditCard: 1234567890 }, schema);
const errMessage = "must have properties 'holderName', 'billingAddress' when property 'creditCard' is present";
expect(errors.errors[0].message).toEqual(errMessage);
expect(errors.errors[0].stack).toEqual(errMessage);
expect(errors.errorSchema).toEqual({
billingAddress: {
__errors: [errMessage],
},
holderName: {
__errors: [errMessage],
},
});
expect(errors.errors[0].params.deps).toEqual('holderName, billingAddress');
});
it('should return an error with title when a dependent is missing', () => {
schema = {
type: 'object',
properties: {
creditCard: {
type: 'number',
title: 'Credit card',
},
billingAddress: {
type: 'string',
title: 'Billing address',
},
},
dependentRequired: {
creditCard: ['billingAddress'],
},
};
const errors = validator.validateFormData({ creditCard: 1234567890 }, schema);
const errMessage = "must have property 'Billing address' when property 'Credit card' is present";
expect(errors.errors[0].message).toEqual(errMessage);
expect(errors.errors[0].stack).toEqual(errMessage);
expect(errors.errorSchema).toEqual({
billingAddress: {
__errors: [errMessage],
},
});
expect(errors.errors[0].params.deps).toEqual('billingAddress');
});
it('should return an error with titles when multiple dependents are missing', () => {
schema = {
type: 'object',
properties: {
creditCard: {
type: 'number',
title: 'Credit card',
},
holderName: {
type: 'string',
title: 'Holder name',
},
billingAddress: {
type: 'string',
title: 'Billing address',
},
},
dependentRequired: {
creditCard: ['holderName', 'billingAddress'],
},
};
const errors = validator.validateFormData({ creditCard: 1234567890 }, schema);
const errMessage =
"must have properties 'Holder name', 'Billing address' when property 'Credit card' is present";
expect(errors.errors[0].message).toEqual(errMessage);
expect(errors.errors[0].stack).toEqual(errMessage);
expect(errors.errorSchema).toEqual({
billingAddress: {
__errors: [errMessage],
},
holderName: {
__errors: [errMessage],
},
});
expect(errors.errors[0].params.deps).toEqual('holderName, billingAddress');
});
it('should return an error with uiSchema title when a dependent is missing', () => {
schema = {
type: 'object',
properties: {
creditCard: {
type: 'number',
},
billingAddress: {
type: 'string',
},
},
dependentRequired: {
creditCard: ['billingAddress'],
},
};
const uiSchema: UiSchema = {
creditCard: {
'ui:title': 'uiSchema Credit card',
},
billingAddress: {
'ui:title': 'uiSchema Billing address',
},
};
const errors = validator.validateFormData({ creditCard: 1234567890 }, schema, undefined, undefined, uiSchema);
const errMessage =
"must have property 'uiSchema Billing address' when property 'uiSchema Credit card' is present";
expect(errors.errors[0].message).toEqual(errMessage);
expect(errors.errors[0].stack).toEqual(errMessage);
expect(errors.errorSchema).toEqual({
billingAddress: {
__errors: [errMessage],
},
});
expect(errors.errors[0].params.deps).toEqual('billingAddress');
});
it('should return an error with uiSchema titles when multiple dependents are missing', () => {
schema = {
type: 'object',
properties: {
creditCard: {
type: 'number',
},
holderName: {
type: 'string',
},
billingAddress: {
type: 'string',
},
},
dependentRequired: {
creditCard: ['holderName', 'billingAddress'],
},
};
const uiSchema: UiSchema = {
creditCard: {
'ui:title': 'uiSchema Credit card',
},
holderName: {
'ui:title': 'uiSchema Holder name',
},
billingAddress: {
'ui:title': 'uiSchema Billing address',
},
};
const errors = validator.validateFormData({ creditCard: 1234567890 }, schema, undefined, undefined, uiSchema);
const errMessage =
"must have properties 'uiSchema Holder name', 'uiSchema Billing address' when property 'uiSchema Credit card' is present";
expect(errors.errors[0].message).toEqual(errMessage);
expect(errors.errors[0].stack).toEqual(errMessage);
expect(errors.errorSchema).toEqual({
billingAddress: {
__errors: [errMessage],
},
holderName: {
__errors: [errMessage],
},
});
expect(errors.errors[0].params.deps).toEqual('holderName, billingAddress');
});
it('should handle the case when errors are not present', () => {
schema = {
type: 'object',
properties: {
creditCard: {
type: 'number',
},
holderName: {
type: 'string',
},
billingAddress: {
type: 'string',
},
},
dependentRequired: {
creditCard: ['holderName', 'billingAddress'],
},
};
const errors = validator.validateFormData(
{
creditCard: 1234567890,
holderName: 'Alice',
billingAddress: 'El Camino Real',
},
schema
);
expect(errors.errors).toHaveLength(0);
});
});
});
describe('validator.validateFormData(), custom options, localizer and Ajv2020', () => {
let validator: AJV8Validator;
Expand Down
Loading