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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ should change the heading of the (upcoming) version to include a major version b

-->

# 5.23.3

## @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
52 changes: 30 additions & 22 deletions packages/validator-ajv8/src/processRawValidationErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,29 +36,33 @@ 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) {
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 +79,10 @@ export function transformRJSFValidationErrors<
}
}

if ('missingProperty' in params) {
property = property ? `${property}.${params.missingProperty}` : params.missingProperty;
}

// put data in expected format
return {
name: keyword,
Expand Down
28 changes: 22 additions & 6 deletions packages/validator-ajv8/src/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,19 +90,35 @@ 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) {
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) {
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
Loading