Skip to content

Commit

Permalink
Partially fixed dependency error messages (#4417)
Browse files Browse the repository at this point in the history
* 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.

* Add comments.

* Update packages/validator-ajv8/src/processRawValidationErrors.ts

---------

Co-authored-by: Heath C <[email protected]>
  • Loading branch information
chibacchie and heath-freenome authored Jan 9, 2025
1 parent b6c1825 commit d211a7d
Show file tree
Hide file tree
Showing 6 changed files with 299 additions and 28 deletions.
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]);
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) {
property = property ? `${property}.${params.missingProperty}` : params.missingProperty;
}

// 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
.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(', ');
}
});
}
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

0 comments on commit d211a7d

Please sign in to comment.