Skip to content

Commit d211a7d

Browse files
Partially fixed dependency error messages (rjsf-team#4417)
* 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. rjsf-team#4402. * Add comments. * Update packages/validator-ajv8/src/processRawValidationErrors.ts --------- Co-authored-by: Heath C <[email protected]>
1 parent b6c1825 commit d211a7d

File tree

6 files changed

+299
-28
lines changed

6 files changed

+299
-28
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ should change the heading of the (upcoming) version to include a major version b
3030
- 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)
3131
- 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).
3232

33+
## @rjsf/validator-ajv8
34+
35+
- 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).
36+
3337
# 5.23.2
3438

3539
## @rjsf/core

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/validator-ajv8/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@types/jest": "^29.5.12",
5353
"@types/json-schema": "^7.0.15",
5454
"@types/lodash": "^4.14.202",
55+
"ajv-i18n": "^4.2.0",
5556
"babel-jest": "^29.7.0",
5657
"eslint": "^8.56.0",
5758
"jest": "^29.7.0",

packages/validator-ajv8/src/processRawValidationErrors.ts

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -36,29 +36,35 @@ export function transformRJSFValidationErrors<
3636
let { message = '' } = rest;
3737
let property = instancePath.replace(/\//g, '.');
3838
let stack = `${property} ${message}`.trim();
39-
40-
if ('missingProperty' in params) {
41-
property = property ? `${property}.${params.missingProperty}` : params.missingProperty;
42-
const currentProperty: string = params.missingProperty;
43-
let uiSchemaTitle = getUiOptions(get(uiSchema, `${property.replace(/^\./, '')}`)).title;
44-
if (uiSchemaTitle === undefined) {
45-
const uiSchemaPath = schemaPath
46-
.replace(/\/properties\//g, '/')
47-
.split('/')
48-
.slice(1, -1)
49-
.concat([currentProperty]);
50-
uiSchemaTitle = getUiOptions(get(uiSchema, uiSchemaPath)).title;
51-
}
52-
53-
if (uiSchemaTitle) {
54-
message = message.replace(`'${currentProperty}'`, `'${uiSchemaTitle}'`);
55-
} else {
56-
const parentSchemaTitle = get(parentSchema, [PROPERTIES_KEY, currentProperty, 'title']);
57-
58-
if (parentSchemaTitle) {
59-
message = message.replace(`'${currentProperty}'`, `'${parentSchemaTitle}'`);
39+
const rawPropertyNames: string[] = [
40+
...(params.deps?.split(', ') || []),
41+
params.missingProperty,
42+
params.property,
43+
].filter((item) => item);
44+
45+
if (rawPropertyNames.length > 0) {
46+
rawPropertyNames.forEach((currentProperty) => {
47+
const path = property ? `${property}.${currentProperty}` : currentProperty;
48+
let uiSchemaTitle = getUiOptions(get(uiSchema, `${path.replace(/^\./, '')}`)).title;
49+
if (uiSchemaTitle === undefined) {
50+
// To retrieve a title from UI schema, construct a path to UI schema from `schemaPath` and `currentProperty`.
51+
// For example, when `#/properties/A/properties/B/required` and `C` are given, they are converted into `['A', 'B', 'C']`.
52+
const uiSchemaPath = schemaPath
53+
.replace(/\/properties\//g, '/')
54+
.split('/')
55+
.slice(1, -1)
56+
.concat([currentProperty]);
57+
uiSchemaTitle = getUiOptions(get(uiSchema, uiSchemaPath)).title;
6058
}
61-
}
59+
if (uiSchemaTitle) {
60+
message = message.replace(`'${currentProperty}'`, `'${uiSchemaTitle}'`);
61+
} else {
62+
const parentSchemaTitle = get(parentSchema, [PROPERTIES_KEY, currentProperty, 'title']);
63+
if (parentSchemaTitle) {
64+
message = message.replace(`'${currentProperty}'`, `'${parentSchemaTitle}'`);
65+
}
66+
}
67+
});
6268

6369
stack = message;
6470
} else {
@@ -75,6 +81,11 @@ export function transformRJSFValidationErrors<
7581
}
7682
}
7783

84+
// If params.missingProperty is undefined, it is removed from rawPropertyNames by filter((item) => item).
85+
if ('missingProperty' in params) {
86+
property = property ? `${property}.${params.missingProperty}` : params.missingProperty;
87+
}
88+
7889
// put data in expected format
7990
return {
8091
name: keyword,

packages/validator-ajv8/src/validator.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,19 +90,38 @@ export default class AJV8Validator<T = any, S extends StrictRJSFSchema = RJSFSch
9090
let errors;
9191
if (compiledValidator) {
9292
if (typeof this.localizer === 'function') {
93-
// Missing properties need to be enclosed with quotes so that
93+
// Properties need to be enclosed with quotes so that
9494
// `AJV8Validator#transformRJSFValidationErrors` replaces property names
95-
// with `title` or `ui:title`. See #4348, #4349, and #4387.
95+
// with `title` or `ui:title`. See #4348, #4349, #4387, and #4402.
9696
(compiledValidator.errors ?? []).forEach((error) => {
97-
if (error.params?.missingProperty) {
98-
error.params.missingProperty = `'${error.params.missingProperty}'`;
97+
['missingProperty', 'property'].forEach((key) => {
98+
if (error.params?.[key]) {
99+
error.params[key] = `'${error.params[key]}'`;
100+
}
101+
});
102+
if (error.params?.deps) {
103+
// As `error.params.deps` is the comma+space separated list of missing dependencies, enclose each dependency separately.
104+
// For example, `A, B` is converted into `'A', 'B'`.
105+
error.params.deps = error.params.deps
106+
.split(', ')
107+
.map((v: string) => `'${v}'`)
108+
.join(', ');
99109
}
100110
});
101111
this.localizer(compiledValidator.errors);
102112
// Revert to originals
103113
(compiledValidator.errors ?? []).forEach((error) => {
104-
if (error.params?.missingProperty) {
105-
error.params.missingProperty = error.params.missingProperty.slice(1, -1);
114+
['missingProperty', 'property'].forEach((key) => {
115+
if (error.params?.[key]) {
116+
error.params[key] = error.params[key].slice(1, -1);
117+
}
118+
});
119+
if (error.params?.deps) {
120+
// Remove surrounding quotes from each missing dependency. For example, `'A', 'B'` is reverted to `A, B`.
121+
error.params.deps = error.params.deps
122+
.split(', ')
123+
.map((v: string) => v.slice(1, -1))
124+
.join(', ');
106125
}
107126
});
108127
}

packages/validator-ajv8/test/validator.test.ts

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
UiSchema,
1010
ValidatorType,
1111
} from '@rjsf/utils';
12+
import localize from 'ajv-i18n';
1213

1314
import AJV8Validator from '../src/validator';
1415
import { Localizer } from '../src';
@@ -2252,6 +2253,240 @@ describe('AJV8Validator', () => {
22522253
});
22532254
});
22542255
});
2256+
describe('validating dependencies', () => {
2257+
beforeAll(() => {
2258+
validator = new AJV8Validator({ AjvClass: Ajv2019 }, localize.en as Localizer);
2259+
});
2260+
it('should return an error when a dependent is missing', () => {
2261+
schema = {
2262+
type: 'object',
2263+
properties: {
2264+
creditCard: {
2265+
type: 'number',
2266+
},
2267+
billingAddress: {
2268+
type: 'string',
2269+
},
2270+
},
2271+
dependentRequired: {
2272+
creditCard: ['billingAddress'],
2273+
},
2274+
};
2275+
const errors = validator.validateFormData({ creditCard: 1234567890 }, schema);
2276+
const errMessage = "must have property 'billingAddress' when property 'creditCard' is present";
2277+
expect(errors.errors[0].message).toEqual(errMessage);
2278+
expect(errors.errors[0].stack).toEqual(errMessage);
2279+
expect(errors.errorSchema).toEqual({
2280+
billingAddress: {
2281+
__errors: [errMessage],
2282+
},
2283+
});
2284+
expect(errors.errors[0].params.deps).toEqual('billingAddress');
2285+
});
2286+
it('should return an error when multiple dependents are missing', () => {
2287+
schema = {
2288+
type: 'object',
2289+
properties: {
2290+
creditCard: {
2291+
type: 'number',
2292+
},
2293+
holderName: {
2294+
type: 'string',
2295+
},
2296+
billingAddress: {
2297+
type: 'string',
2298+
},
2299+
},
2300+
dependentRequired: {
2301+
creditCard: ['holderName', 'billingAddress'],
2302+
},
2303+
};
2304+
const errors = validator.validateFormData({ creditCard: 1234567890 }, schema);
2305+
const errMessage = "must have properties 'holderName', 'billingAddress' when property 'creditCard' is present";
2306+
expect(errors.errors[0].message).toEqual(errMessage);
2307+
expect(errors.errors[0].stack).toEqual(errMessage);
2308+
expect(errors.errorSchema).toEqual({
2309+
billingAddress: {
2310+
__errors: [errMessage],
2311+
},
2312+
holderName: {
2313+
__errors: [errMessage],
2314+
},
2315+
});
2316+
expect(errors.errors[0].params.deps).toEqual('holderName, billingAddress');
2317+
});
2318+
it('should return an error with title when a dependent is missing', () => {
2319+
schema = {
2320+
type: 'object',
2321+
properties: {
2322+
creditCard: {
2323+
type: 'number',
2324+
title: 'Credit card',
2325+
},
2326+
billingAddress: {
2327+
type: 'string',
2328+
title: 'Billing address',
2329+
},
2330+
},
2331+
dependentRequired: {
2332+
creditCard: ['billingAddress'],
2333+
},
2334+
};
2335+
const errors = validator.validateFormData({ creditCard: 1234567890 }, schema);
2336+
const errMessage = "must have property 'Billing address' when property 'Credit card' is present";
2337+
expect(errors.errors[0].message).toEqual(errMessage);
2338+
expect(errors.errors[0].stack).toEqual(errMessage);
2339+
expect(errors.errorSchema).toEqual({
2340+
billingAddress: {
2341+
__errors: [errMessage],
2342+
},
2343+
});
2344+
expect(errors.errors[0].params.deps).toEqual('billingAddress');
2345+
});
2346+
it('should return an error with titles when multiple dependents are missing', () => {
2347+
schema = {
2348+
type: 'object',
2349+
properties: {
2350+
creditCard: {
2351+
type: 'number',
2352+
title: 'Credit card',
2353+
},
2354+
holderName: {
2355+
type: 'string',
2356+
title: 'Holder name',
2357+
},
2358+
billingAddress: {
2359+
type: 'string',
2360+
title: 'Billing address',
2361+
},
2362+
},
2363+
dependentRequired: {
2364+
creditCard: ['holderName', 'billingAddress'],
2365+
},
2366+
};
2367+
const errors = validator.validateFormData({ creditCard: 1234567890 }, schema);
2368+
const errMessage =
2369+
"must have properties 'Holder name', 'Billing address' when property 'Credit card' is present";
2370+
expect(errors.errors[0].message).toEqual(errMessage);
2371+
expect(errors.errors[0].stack).toEqual(errMessage);
2372+
expect(errors.errorSchema).toEqual({
2373+
billingAddress: {
2374+
__errors: [errMessage],
2375+
},
2376+
holderName: {
2377+
__errors: [errMessage],
2378+
},
2379+
});
2380+
expect(errors.errors[0].params.deps).toEqual('holderName, billingAddress');
2381+
});
2382+
it('should return an error with uiSchema title when a dependent is missing', () => {
2383+
schema = {
2384+
type: 'object',
2385+
properties: {
2386+
creditCard: {
2387+
type: 'number',
2388+
},
2389+
billingAddress: {
2390+
type: 'string',
2391+
},
2392+
},
2393+
dependentRequired: {
2394+
creditCard: ['billingAddress'],
2395+
},
2396+
};
2397+
const uiSchema: UiSchema = {
2398+
creditCard: {
2399+
'ui:title': 'uiSchema Credit card',
2400+
},
2401+
billingAddress: {
2402+
'ui:title': 'uiSchema Billing address',
2403+
},
2404+
};
2405+
const errors = validator.validateFormData({ creditCard: 1234567890 }, schema, undefined, undefined, uiSchema);
2406+
const errMessage =
2407+
"must have property 'uiSchema Billing address' when property 'uiSchema Credit card' is present";
2408+
expect(errors.errors[0].message).toEqual(errMessage);
2409+
expect(errors.errors[0].stack).toEqual(errMessage);
2410+
expect(errors.errorSchema).toEqual({
2411+
billingAddress: {
2412+
__errors: [errMessage],
2413+
},
2414+
});
2415+
expect(errors.errors[0].params.deps).toEqual('billingAddress');
2416+
});
2417+
it('should return an error with uiSchema titles when multiple dependents are missing', () => {
2418+
schema = {
2419+
type: 'object',
2420+
properties: {
2421+
creditCard: {
2422+
type: 'number',
2423+
},
2424+
holderName: {
2425+
type: 'string',
2426+
},
2427+
billingAddress: {
2428+
type: 'string',
2429+
},
2430+
},
2431+
dependentRequired: {
2432+
creditCard: ['holderName', 'billingAddress'],
2433+
},
2434+
};
2435+
const uiSchema: UiSchema = {
2436+
creditCard: {
2437+
'ui:title': 'uiSchema Credit card',
2438+
},
2439+
holderName: {
2440+
'ui:title': 'uiSchema Holder name',
2441+
},
2442+
billingAddress: {
2443+
'ui:title': 'uiSchema Billing address',
2444+
},
2445+
};
2446+
const errors = validator.validateFormData({ creditCard: 1234567890 }, schema, undefined, undefined, uiSchema);
2447+
const errMessage =
2448+
"must have properties 'uiSchema Holder name', 'uiSchema Billing address' when property 'uiSchema Credit card' is present";
2449+
expect(errors.errors[0].message).toEqual(errMessage);
2450+
expect(errors.errors[0].stack).toEqual(errMessage);
2451+
expect(errors.errorSchema).toEqual({
2452+
billingAddress: {
2453+
__errors: [errMessage],
2454+
},
2455+
holderName: {
2456+
__errors: [errMessage],
2457+
},
2458+
});
2459+
expect(errors.errors[0].params.deps).toEqual('holderName, billingAddress');
2460+
});
2461+
it('should handle the case when errors are not present', () => {
2462+
schema = {
2463+
type: 'object',
2464+
properties: {
2465+
creditCard: {
2466+
type: 'number',
2467+
},
2468+
holderName: {
2469+
type: 'string',
2470+
},
2471+
billingAddress: {
2472+
type: 'string',
2473+
},
2474+
},
2475+
dependentRequired: {
2476+
creditCard: ['holderName', 'billingAddress'],
2477+
},
2478+
};
2479+
const errors = validator.validateFormData(
2480+
{
2481+
creditCard: 1234567890,
2482+
holderName: 'Alice',
2483+
billingAddress: 'El Camino Real',
2484+
},
2485+
schema
2486+
);
2487+
expect(errors.errors).toHaveLength(0);
2488+
});
2489+
});
22552490
});
22562491
describe('validator.validateFormData(), custom options, localizer and Ajv2020', () => {
22572492
let validator: AJV8Validator;

0 commit comments

Comments
 (0)