Skip to content

Commit 4b9c0e7

Browse files
Feature: validator-ajv8: Add support for suppressing duplicate filtering (#5032)
* Feature: validator-ajv8: Add support for suppressing duplicate filtering Fixes #5028 by adding configuration support to the `validator-ajv8` to suppress duplicate filtering - Updated `CustomValidatorOptionsType` to add new `suppressDuplicateFiltering` flag prop - Updated `AJV8Validator`, `AJV8PrecompiledValidator` and `createPrecompiledValidator()` to support passing the new flag prop - Updated `processRawValidationErrors()` to pass the new flag prop into `transformRJSFValidationErrors()` - Refactored the filtering of duplicates to a new `filterDuplicateErrors()` function that takes the new flag prop and suppresses the appropriate filtering - Updated the unit tests to verify the flag works as intended - Updated the `validator-ajv8.md` and `validation.md` docs for the new flag prop - Updated the `CHANGELOG.md` accordingly * - Responded to reviewer feedback. Added 'none' value * - Updated example schema in `validation.md` to match schema from #5028
1 parent 997325e commit 4b9c0e7

File tree

12 files changed

+330
-38
lines changed

12 files changed

+330
-38
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,15 @@ should change the heading of the (upcoming) version to include a major version b
7777
- Updated `getInputProps()` to propagate `formatMinimum` and `formatMaximum` schema keywords to the HTML `min`/`max` attributes for `date`, `datetime-local`, `time`, `week`, and `month` input types, aligning browser-native date picker constraints with AJV validation
7878
- Fixed `ui:title` from `ui:definitions` not applied to `oneOf`/`anyOf` dropdowns beyond first recursion level, fixing [#4986](https://github.com/rjsf-team/react-jsonschema-form/issues/4986)
7979

80+
# @rjsf/validator-ajv8
81+
82+
- Updated `CustomValidatorOptionsType` to add the new `suppressDuplicateFiltering` flag prop, updating the validators to pass it through to the `transformRJSFValidationErrors()` which suppresses the appropriate duplicate errors if specified, fixing [#5028](https://github.com/rjsf-team/react-jsonschema-form/issues/5028)
83+
8084
## Dev / docs / playground
8185

8286
- Updated References playground sample to demonstrate `oneOf` with `ui:title` at recursive depth, related to [#4986](https://github.com/rjsf-team/react-jsonschema-form/issues/4986)
8387
- Updated the building of the `mantine` theme to properly support ESM, fixing [#5025](https://github.com/rjsf-team/react-jsonschema-form/issues/5025)
88+
- Updated the `validator-ajv8.md` and `validation.md` documetation for the new `suppressDuplicateFiltering` configuration prop
8489

8590
# 6.4.2
8691

packages/docs/docs/api-reference/validator-ajv8.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,14 @@ If a `localizer` is provided, it is used to translate the messages generated by
2020

2121
#### Parameters
2222

23-
- [options={}]: CustomValidatorOptionsType - The optional map of `CustomValidatorOptionsType` options that are used to create the `ValidatorType` instance
23+
- [options={}]: CustomValidatorOptionsType - The optional map of `CustomValidatorOptionsType` options that are used to create the `ValidatorType` instance. Supported options:
24+
- `additionalMetaSchemas`: Additional meta schemas the validator can access
25+
- `customFormats`: Additional custom format validators
26+
- `ajvOptionsOverrides`: Config overrides passed to the AJV constructor
27+
- `ajvFormatOptions`: Options for `ajv-formats`; pass `false` to disable format support entirely
28+
- `AjvClass`: The AJV class to construct (e.g. `Ajv2019`, `Ajv2020`)
29+
- `extenderFn`: A function called to extend the AJV instance (e.g. `ajvErrors()`)
30+
- `suppressDuplicateFiltering`: Controls filtering of duplicate `anyOf`/`oneOf` errors using the `SuppressDuplicateFilteringType` type. `'none'` (default) filters duplicates for both; `'anyOf'` suppresses filtering for `anyOf` (oneOf duplicates still filtered); `'oneOf'` suppresses filtering for `oneOf` (anyOf duplicates still filtered); `'all'` disables all duplicate filtering.
2431
- [localizer]: Localizer | undefined - If provided, is used to localize a list of Ajv `ErrorObject`s after running the form validation using AJV
2532

2633
#### Returns
@@ -51,6 +58,7 @@ If a `localizer` is provided, it is used to translate the messages generated by
5158
- validateFns: ValidatorFunctions - The map of the validation functions that are created by the `compileSchemaValidators()` function
5259
- rootSchema: S - The root schema that was used with the `compileSchemaValidators()` function
5360
- [localizer]: Localizer | undefined - If provided, is used to localize a list of Ajv `ErrorObject`s after running the form validation using AJV
61+
- [suppressDuplicateFiltering]: `SuppressDuplicateFilteringType | undefined` - Controls filtering of duplicate `anyOf`/`oneOf` errors. See `customizeValidator()` for full description of each value.
5462

5563
#### Returns
5664

packages/docs/docs/usage/validation.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,78 @@ const validator = customizeValidator({ extenderFn: ajvErrors });
707707
render(<Form schema={schema} validator={validator} />, document.getElementById('app'));
708708
```
709709

710+
### suppressDuplicateFiltering
711+
712+
When AJV validates against `anyOf` or `oneOf` schemas, it generates a separate error for each failed branch, which often produces many duplicate messages.
713+
By default, the validator filters out these duplicates so that only the first occurrence of each unique error is shown.
714+
Use `suppressDuplicateFiltering` to override this behavior when you need to see all errors from every branch.
715+
716+
The option accepts a `SuppressDuplicateFilteringType` value (exported from `@rjsf/validator-ajv8`):
717+
718+
| Value | Behavior |
719+
| ------------------ | --------------------------------------------------------------------------- |
720+
| `'none'` (default) | Duplicates filtered for both `anyOf` and `oneOf` |
721+
| `'anyOf'` | `anyOf` duplicate filtering disabled; `oneOf` duplicates still filtered |
722+
| `'oneOf'` | `oneOf` duplicate filtering disabled; `anyOf` duplicates still filtered |
723+
| `'all'` | All duplicate filtering disabled; every error from every branch is returned |
724+
725+
For a standard validator via `customizeValidator()`:
726+
727+
```tsx
728+
import { Form } from '@rjsf/core';
729+
import { RJSFSchema } from '@rjsf/utils';
730+
import { customizeValidator } from '@rjsf/validator-ajv8';
731+
732+
const schema: RJSFSchema = {
733+
type: 'object',
734+
properties: {
735+
asc: {
736+
title: 'Low to high',
737+
type: 'boolean',
738+
default: true,
739+
},
740+
desc: {
741+
title: 'High to low',
742+
type: 'boolean',
743+
default: true,
744+
},
745+
},
746+
anyOf: [
747+
{
748+
required: ['asc'],
749+
properties: {
750+
asc: {
751+
const: true,
752+
},
753+
},
754+
},
755+
{
756+
required: ['desc'],
757+
properties: {
758+
desc: {
759+
const: true,
760+
},
761+
},
762+
},
763+
],
764+
};
765+
766+
// Disable all duplicate filtering — show every error from every anyOf/oneOf branch
767+
const validator = customizeValidator({ suppressDuplicateFiltering: 'all' });
768+
769+
render(<Form schema={schema} validator={validator} />, document.getElementById('app'));
770+
```
771+
772+
For a precompiled validator, pass the value as the fourth argument to `createPrecompiledValidator()`:
773+
774+
```tsx
775+
import { createPrecompiledValidator } from '@rjsf/validator-ajv8';
776+
import * as precompiledValidatorFns from 'path_to/yourCompiledSchema';
777+
import yourSchema from 'path_to/yourSchema';
778+
779+
const validator = createPrecompiledValidator(precompiledValidatorFns, yourSchema, undefined, 'all');
780+
```
781+
710782
### Localization (L10n) support
711783

712784
The Ajv 8 validator supports the localization of error messages using [ajv-i18n](https://github.com/ajv-validator/ajv-i18n).

packages/validator-ajv8/src/createPrecompiledValidator.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { FormContextType, RJSFSchema, StrictRJSFSchema, ValidatorType } from '@rjsf/utils';
22

3-
import { Localizer, ValidatorFunctions } from './types';
3+
import { Localizer, SuppressDuplicateFilteringType, ValidatorFunctions } from './types';
44
import AJV8PrecompiledValidator from './precompiledValidator';
55

66
/** Creates and returns a `ValidatorType` interface that is implemented with a precompiled validator. If a `localizer`
@@ -12,12 +12,18 @@ import AJV8PrecompiledValidator from './precompiledValidator';
1212
* @param validateFns - The map of the validation functions that are created by the `compileSchemaValidators()` function
1313
* @param rootSchema - The root schema that was used with the `compileSchemaValidators()` function
1414
* @param [localizer] - If provided, is used to localize a list of Ajv `ErrorObject`s
15+
* @param [suppressDuplicateFiltering] - Controls which duplicate filtering is suppressed; see `filterDuplicateErrors`
1516
* @returns - The precompiled validator implementation resulting from the set of parameters provided
1617
*/
1718
export default function createPrecompiledValidator<
1819
T = any,
1920
S extends StrictRJSFSchema = RJSFSchema,
2021
F extends FormContextType = any,
21-
>(validateFns: ValidatorFunctions, rootSchema: S, localizer?: Localizer): ValidatorType<T, S, F> {
22-
return new AJV8PrecompiledValidator<T, S, F>(validateFns, rootSchema, localizer);
22+
>(
23+
validateFns: ValidatorFunctions,
24+
rootSchema: S,
25+
localizer?: Localizer,
26+
suppressDuplicateFiltering?: SuppressDuplicateFilteringType,
27+
): ValidatorType<T, S, F> {
28+
return new AJV8PrecompiledValidator<T, S, F>(validateFns, rootSchema, localizer, suppressDuplicateFiltering);
2329
}

packages/validator-ajv8/src/precompiledValidator.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
ValidatorType,
1717
} from '@rjsf/utils';
1818

19-
import { CompiledValidateFunction, Localizer, ValidatorFunctions } from './types';
19+
import { CompiledValidateFunction, Localizer, SuppressDuplicateFilteringType, ValidatorFunctions } from './types';
2020
import processRawValidationErrors, { RawValidationErrorsType } from './processRawValidationErrors';
2121

2222
/** `ValidatorType` implementation that uses an AJV 8 precompiled validator as created by the
@@ -51,17 +51,30 @@ export default class AJV8PrecompiledValidator<
5151
*/
5252
readonly localizer?: Localizer;
5353

54+
/** Controls which duplicate error filtering is suppressed; see `filterDuplicateErrors`
55+
*
56+
* @private
57+
*/
58+
readonly suppressDuplicateFiltering?: SuppressDuplicateFilteringType;
59+
5460
/** Constructs an `AJV8PrecompiledValidator` instance using the `validateFns` and `rootSchema`
5561
*
5662
* @param validateFns - The map of the validation functions that are generated by the `schemaCompile()` function
5763
* @param rootSchema - The root schema that was used with the `compileSchema()` function
5864
* @param [localizer] - If provided, is used to localize a list of Ajv `ErrorObject`s
65+
* @param [suppressDuplicateFiltering] - Controls which duplicate filtering is suppressed; see `filterDuplicateErrors`
5966
* @throws - Error when the base schema of the precompiled validator does not have a matching validator function
6067
*/
61-
constructor(validateFns: ValidatorFunctions, rootSchema: S, localizer?: Localizer) {
68+
constructor(
69+
validateFns: ValidatorFunctions,
70+
rootSchema: S,
71+
localizer?: Localizer,
72+
suppressDuplicateFiltering?: SuppressDuplicateFilteringType,
73+
) {
6274
this.rootSchema = rootSchema;
6375
this.validateFns = validateFns;
6476
this.localizer = localizer;
77+
this.suppressDuplicateFiltering = suppressDuplicateFiltering;
6578
this.mainValidator = this.getValidator(rootSchema);
6679
}
6780

@@ -142,7 +155,16 @@ export default class AJV8PrecompiledValidator<
142155
uiSchema?: UiSchema<T, S, F>,
143156
): ValidationData<T> {
144157
const rawErrors = this.rawValidation<ErrorObject>(schema, formData);
145-
return processRawValidationErrors(this, rawErrors, formData, schema, customValidate, transformErrors, uiSchema);
158+
return processRawValidationErrors(
159+
this,
160+
rawErrors,
161+
formData,
162+
schema,
163+
customValidate,
164+
transformErrors,
165+
uiSchema,
166+
this.suppressDuplicateFiltering,
167+
);
146168
}
147169

148170
/** Validates data against a schema, returning true if the data is valid, or false otherwise. If the schema is

packages/validator-ajv8/src/processRawValidationErrors.ts

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,68 @@ import {
2020
ValidatorType,
2121
} from '@rjsf/utils';
2222

23+
import { SuppressDuplicateFilteringType } from './types';
24+
2325
export type RawValidationErrorsType<Result = any> = {
2426
errors?: Result[];
2527
validationError?: Error;
2628
};
2729

30+
/** Filters duplicate errors from `anyOf`/`oneOf` schema paths according to the `suppressDuplicateFiltering` flag.
31+
*
32+
* @param errorList - The list of `RJSFValidationError`s to filter
33+
* @param [suppressDuplicateFiltering='none'] - Controls which duplicate filtering is suppressed:
34+
* - `'none'` (default): filters duplicates for both `anyOf` and `oneOf`
35+
* - `'all'`: returns `errorList` unmodified
36+
* - `'anyOf'`: suppresses filtering for `anyOf` errors; `oneOf` duplicates are still filtered
37+
* - `'oneOf'`: suppresses filtering for `oneOf` errors; `anyOf` duplicates are still filtered
38+
*/
39+
export function filterDuplicateErrors(
40+
errorList: RJSFValidationError[],
41+
suppressDuplicateFiltering: SuppressDuplicateFilteringType = 'none',
42+
): RJSFValidationError[] {
43+
if (suppressDuplicateFiltering === 'all') {
44+
return errorList;
45+
}
46+
return errorList.reduce((acc: RJSFValidationError[], err: RJSFValidationError) => {
47+
const { message, schemaPath } = err;
48+
// Compute the index only when filtering for that keyword is not suppressed.
49+
// 'all' is already handled above; within the reduce, only 'none', 'anyOf', and 'oneOf' are possible.
50+
const anyOfIndex = suppressDuplicateFiltering !== 'anyOf' ? schemaPath?.indexOf(`/${ANY_OF_KEY}/`) : undefined;
51+
const oneOfIndex = suppressDuplicateFiltering !== 'oneOf' ? schemaPath?.indexOf(`/${ONE_OF_KEY}/`) : undefined;
52+
let schemaPrefix: string | undefined;
53+
if (anyOfIndex && anyOfIndex >= 0) {
54+
schemaPrefix = schemaPath?.substring(0, anyOfIndex);
55+
} else if (oneOfIndex && oneOfIndex >= 0) {
56+
schemaPrefix = schemaPath?.substring(0, oneOfIndex);
57+
}
58+
// If there is a schemaPrefix, then search for a duplicate message with the same prefix, otherwise undefined
59+
const dup = schemaPrefix
60+
? acc.find((e: RJSFValidationError) => e.message === message && e.schemaPath?.startsWith(schemaPrefix))
61+
: undefined;
62+
if (!dup) {
63+
acc.push(err);
64+
}
65+
return acc;
66+
}, [] as RJSFValidationError[]);
67+
}
68+
2869
/** Transforming the error output from ajv to format used by @rjsf/utils.
2970
* At some point, components should be updated to support ajv.
3071
*
3172
* @param errors - The list of AJV errors to convert to `RJSFValidationErrors`
3273
* @param [uiSchema] - An optional uiSchema that is passed to `transformErrors` and `customValidate`
74+
* @param [suppressDuplicateFiltering] - Controls which duplicate filtering is suppressed; see `filterDuplicateErrors`
3375
*/
3476
export function transformRJSFValidationErrors<
3577
T = any,
3678
S extends StrictRJSFSchema = RJSFSchema,
3779
F extends FormContextType = any,
38-
>(errors: ErrorObject[] = [], uiSchema?: UiSchema<T, S, F>): RJSFValidationError[] {
80+
>(
81+
errors: ErrorObject[] = [],
82+
uiSchema?: UiSchema<T, S, F>,
83+
suppressDuplicateFiltering?: SuppressDuplicateFilteringType,
84+
): RJSFValidationError[] {
3985
const errorList = errors.map((e: ErrorObject) => {
4086
const { instancePath, keyword, params, schemaPath, parentSchema, ...rest } = e;
4187
let { message = '' } = rest;
@@ -107,28 +153,7 @@ export function transformRJSFValidationErrors<
107153
title: uiTitle,
108154
};
109155
});
110-
// Filter out duplicates around anyOf/oneOf messages
111-
return errorList.reduce((acc: RJSFValidationError[], err: RJSFValidationError) => {
112-
const { message, schemaPath } = err;
113-
const anyOfIndex = schemaPath?.indexOf(`/${ANY_OF_KEY}/`);
114-
const oneOfIndex = schemaPath?.indexOf(`/${ONE_OF_KEY}/`);
115-
let schemaPrefix: string | undefined;
116-
// Look specifically for `/anyOr/` or `/oneOf/` within the schemaPath information
117-
if (anyOfIndex && anyOfIndex >= 0) {
118-
schemaPrefix = schemaPath?.substring(0, anyOfIndex);
119-
} else if (oneOfIndex && oneOfIndex >= 0) {
120-
schemaPrefix = schemaPath?.substring(0, oneOfIndex);
121-
}
122-
// If there is a schemaPrefix, then search for a duplicate message with the same prefix, otherwise undefined
123-
const dup = schemaPrefix
124-
? acc.find((e: RJSFValidationError) => e.message === message && e.schemaPath?.startsWith(schemaPrefix))
125-
: undefined;
126-
if (!dup) {
127-
// Only push an error that is not a duplicate
128-
acc.push(err);
129-
}
130-
return acc;
131-
}, [] as RJSFValidationError[]);
156+
return filterDuplicateErrors(errorList, suppressDuplicateFiltering);
132157
}
133158

134159
/** This function processes the `formData` with an optional user contributed `customValidate` function, which receives
@@ -143,6 +168,7 @@ export function transformRJSFValidationErrors<
143168
* @param [customValidate] - An optional function that is used to perform custom validation
144169
* @param [transformErrors] - An optional function that is used to transform errors after AJV validation
145170
* @param [uiSchema] - An optional uiSchema that is passed to `transformErrors` and `customValidate`
171+
* @param [suppressDuplicateFiltering] - Controls which duplicate filtering is suppressed; see `filterDuplicateErrors`
146172
*/
147173
export default function processRawValidationErrors<
148174
T = any,
@@ -156,9 +182,10 @@ export default function processRawValidationErrors<
156182
customValidate?: CustomValidator<T, S, F>,
157183
transformErrors?: ErrorTransformer<T, S, F>,
158184
uiSchema?: UiSchema<T, S, F>,
185+
suppressDuplicateFiltering?: SuppressDuplicateFilteringType,
159186
) {
160187
const { validationError: invalidSchemaError } = rawErrors;
161-
let errors = transformRJSFValidationErrors<T, S, F>(rawErrors.errors, uiSchema);
188+
let errors = transformRJSFValidationErrors<T, S, F>(rawErrors.errors, uiSchema, suppressDuplicateFiltering);
162189

163190
if (invalidSchemaError) {
164191
errors = [...errors, { stack: invalidSchemaError!.message }];

packages/validator-ajv8/src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import Ajv, { Options, ErrorObject } from 'ajv';
22
import { FormatsPluginOptions } from 'ajv-formats';
33

4+
/** The type describing the value for the `suppressDuplicateFiltering` option */
5+
export type SuppressDuplicateFilteringType = 'anyOf' | 'oneOf' | 'all' | 'none';
6+
47
/** The type describing how to customize the AJV6 validator
58
*/
69
export interface CustomValidatorOptionsType {
@@ -18,6 +21,13 @@ export interface CustomValidatorOptionsType {
1821
AjvClass?: typeof Ajv;
1922
/** A function to call to extend AJV, such as `ajvErrors()` */
2023
extenderFn?: (ajv: Ajv) => Ajv;
24+
/** When set, suppresses duplicate error filtering for the specified keyword(s):
25+
* - `'none'` (default): both `anyOf` and `oneOf` duplicate errors are filtered
26+
* - `'all'`: disables all duplicate filtering
27+
* - `'anyOf'`: disables filtering for `anyOf` errors only (oneOf duplicates are still filtered)
28+
* - `'oneOf'`: disables filtering for `oneOf` errors only (anyOf duplicates are still filtered)
29+
*/
30+
suppressDuplicateFiltering?: SuppressDuplicateFilteringType;
2131
}
2232

2333
/** The type describing a function that takes a list of Ajv `ErrorObject`s and localizes them

0 commit comments

Comments
 (0)