Skip to content

Commit 673c3f2

Browse files
Fix 4167 and 4746 in validator-ajv8 (#4884)
* Fix 4167 and 4746 in validator-ajv8 Fixed #4167 by adding filtering of duplicate messages related to `anyOf` and `oneOf` Fixed #4146 by adding new `extenderFn` option to the `CustomValidatorOptionsType` used by validators and precompiled validators - In `@rjsf/validator-ajv8`: - Updated `CustomValidatorOptionsType` to add new `extenderFn?: (ajv: Ajv) => Ajv` prop - Updated `createAjvInstance()` to add new `extenderFn?: (ajv: Ajv) => Ajv` parameter, using it to extend the `ajv` instance - Updated the `AJV8Validator` and `compileSchemaValidatorsCode()` to pass `extenderFn` from the `options` into `createAjvInstance()` - Updated `transformRJSFValidationErrors()` to add filtering of duplicate `anyOf`/`oneOf` based errors from the returned errors - Updated `validation.md` to document the new `extenderFn` feature - Updated `CHANGELOG.md` accordingly * - improved docs * - more improvements
1 parent 8543741 commit 673c3f2

File tree

11 files changed

+485
-9
lines changed

11 files changed

+485
-9
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ should change the heading of the (upcoming) version to include a major version b
3232

3333
- Replace json-schema-merge-allof with [@x0k/json-schema-merge](https://github.com/x0k/json-schema-merge/) ([#4774](https://github.com/rjsf-team/react-jsonschema-form/issues/4774))
3434

35+
## @rjsf/validator-ajv8
36+
37+
- Updated `CustomValidatorOptionsType` to add new `extenderFn?: (ajv: Ajv) => Ajv` prop
38+
- Updated `createAjvInstance()` to add new `extenderFn?: (ajv: Ajv) => Ajv` parameter, using it to extend the `ajv` instance, fixing [#4746](https://github.com/rjsf-team/react-jsonschema-form/issues/4746)
39+
- Updated the `AJV8Validator` and `compileSchemaValidatorsCode()` to pass `extenderFn` from the `options` into `createAjvInstance()`
40+
- Updated `transformRJSFValidationErrors()` to add filtering of duplicate `anyOf`/`oneOf` based errors from the returned errors, fixing [#4167](https://github.com/rjsf-team/react-jsonschema-form/issues/4167)
41+
3542
## Dev / docs / playground
3643

3744
- Updated `DemoFrame` as follows to fix [#3609](https://github.com/rjsf-team/react-jsonschema-form/issues/3609)
@@ -40,6 +47,7 @@ should change the heading of the (upcoming) version to include a major version b
4047
- Update the `antd` theme wrapper to render the `AntdSelectPatcher`, `AntdStyleProvider` and `ConfigProvider` with it's own `getPopupContainer()` function inside of a `FrameContextConsumer`
4148
- Updated the base TypeScript configuration to use `"moduleResolution": "bundler"`
4249
- Updated the `validation.md` documentation to note that HTML 5 validation is not translatable via RJSF translation mechanisms and should be turned off, fixing [#4092](https://github.com/rjsf-team/react-jsonschema-form/issues/4092)
50+
- Also added documentation for the new `extenderFn` prop on `CustomValidatorOptionsType`
4351

4452
# 6.1.1
4553

packages/docs/docs/usage/validation.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,26 @@ const validator = customizeValidator({ AjvClass: Ajv2019 });
686686
render(<Form schema={schema} validator={validator} />, document.getElementById('app'));
687687
```
688688

689+
### extenderFn
690+
691+
If you need to use an additional library, such as `ajv-errors`, with our validators you can do so by creating a custom validator and pass it the `ajv` library "extender" via the `extenderFn` prop on the `options` parameter.
692+
693+
```tsx
694+
import { Form } from '@rjsf/core';
695+
import { RJSFSchema } from '@rjsf/utils';
696+
import { customizeValidator } from '@rjsf/validator-ajv8';
697+
import ajvErrors from 'ajv-errors';
698+
699+
const schema: RJSFSchema = {
700+
type: 'string',
701+
format: 'date',
702+
};
703+
704+
const validator = customizeValidator({ extenderFn: ajvErrors });
705+
706+
render(<Form schema={schema} validator={validator} />, document.getElementById('app'));
707+
```
708+
689709
### Localization (L10n) support
690710

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

packages/validator-ajv8/src/compileSchemaValidatorsCode.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,28 @@ export function compileSchemaValidatorsCode<S extends StrictRJSFSchema = RJSFSch
2121
const schemaMaps = schemaParser(schema);
2222
const schemas = Object.values(schemaMaps);
2323

24-
const { additionalMetaSchemas, customFormats, ajvOptionsOverrides = {}, ajvFormatOptions, AjvClass } = options;
24+
const {
25+
additionalMetaSchemas,
26+
customFormats,
27+
ajvOptionsOverrides = {},
28+
ajvFormatOptions,
29+
AjvClass,
30+
extenderFn,
31+
} = options;
2532
// Allow users to turn off the `lines: true` feature in their own overrides, but NOT the `source: true`
2633
const compileOptions = {
2734
...ajvOptionsOverrides,
2835
code: { lines: true, ...ajvOptionsOverrides.code, source: true },
2936
schemas,
3037
};
31-
const ajv = createAjvInstance(additionalMetaSchemas, customFormats, compileOptions, ajvFormatOptions, AjvClass);
38+
const ajv = createAjvInstance(
39+
additionalMetaSchemas,
40+
customFormats,
41+
compileOptions,
42+
ajvFormatOptions,
43+
AjvClass,
44+
extenderFn,
45+
);
3246

3347
return standaloneCode(ajv);
3448
}

packages/validator-ajv8/src/createAjvInstance.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,17 @@ export const DATA_URL_FORMAT_REGEX = /^data:([a-z]+\/[a-z0-9-+.]+)?;(?:name=(.*)
3030
* @param [ajvOptionsOverrides={}] - The set of validator config override options
3131
* @param [ajvFormatOptions] - The `ajv-format` options to use when adding formats to `ajv`; pass `false` to disable it
3232
* @param [AjvClass] - The `Ajv` class to use when creating the validator instance
33+
* @param [extenderFn] - A function to call to extend AJV, such as `ajvErrors()`
3334
*/
3435
export default function createAjvInstance(
3536
additionalMetaSchemas?: CustomValidatorOptionsType['additionalMetaSchemas'],
3637
customFormats?: CustomValidatorOptionsType['customFormats'],
3738
ajvOptionsOverrides: CustomValidatorOptionsType['ajvOptionsOverrides'] = {},
3839
ajvFormatOptions?: FormatsPluginOptions | false,
3940
AjvClass: typeof Ajv = Ajv,
41+
extenderFn?: CustomValidatorOptionsType['extenderFn'],
4042
) {
41-
const ajv = new AjvClass({ ...AJV_CONFIG, ...ajvOptionsOverrides });
43+
let ajv = new AjvClass({ ...AJV_CONFIG, ...ajvOptionsOverrides });
4244
if (ajvFormatOptions) {
4345
addFormats(ajv, ajvFormatOptions);
4446
} else if (ajvFormatOptions !== false) {
@@ -64,6 +66,9 @@ export default function createAjvInstance(
6466
ajv.addFormat(formatName, customFormats[formatName]);
6567
});
6668
}
69+
if (extenderFn) {
70+
ajv = extenderFn(ajv);
71+
}
6772

6873
return ajv;
6974
}

packages/validator-ajv8/src/processRawValidationErrors.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { ErrorObject } from 'ajv';
22
import get from 'lodash/get';
33
import {
4+
ANY_OF_KEY,
45
createErrorHandler,
56
CustomValidator,
67
ErrorTransformer,
78
FormContextType,
89
getDefaultFormState,
910
getUiOptions,
11+
ONE_OF_KEY,
1012
PROPERTIES_KEY,
1113
RJSFSchema,
1214
RJSFValidationError,
@@ -34,7 +36,7 @@ export function transformRJSFValidationErrors<
3436
S extends StrictRJSFSchema = RJSFSchema,
3537
F extends FormContextType = any,
3638
>(errors: ErrorObject[] = [], uiSchema?: UiSchema<T, S, F>): RJSFValidationError[] {
37-
return errors.map((e: ErrorObject) => {
39+
const errorList = errors.map((e: ErrorObject) => {
3840
const { instancePath, keyword, params, schemaPath, parentSchema, ...rest } = e;
3941
let { message = '' } = rest;
4042
let property = instancePath.replace(/\//g, '.');
@@ -105,6 +107,28 @@ export function transformRJSFValidationErrors<
105107
title: uiTitle,
106108
};
107109
});
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[]);
108132
}
109133

110134
/** This function processes the `formData` with an optional user contributed `customValidate` function, which receives

packages/validator-ajv8/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export interface CustomValidatorOptionsType {
1616
ajvFormatOptions?: FormatsPluginOptions | false;
1717
/** The AJV class to construct */
1818
AjvClass?: typeof Ajv;
19+
/** A function to call to extend AJV, such as `ajvErrors()` */
20+
extenderFn?: (ajv: Ajv) => Ajv;
1921
}
2022

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

packages/validator-ajv8/src/validator.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,16 @@ export default class AJV8Validator<T = any, S extends StrictRJSFSchema = RJSFSch
4242
* @param [localizer] - If provided, is used to localize a list of Ajv `ErrorObject`s
4343
*/
4444
constructor(options: CustomValidatorOptionsType, localizer?: Localizer) {
45-
const { additionalMetaSchemas, customFormats, ajvOptionsOverrides, ajvFormatOptions, AjvClass } = options;
46-
this.ajv = createAjvInstance(additionalMetaSchemas, customFormats, ajvOptionsOverrides, ajvFormatOptions, AjvClass);
45+
const { additionalMetaSchemas, customFormats, ajvOptionsOverrides, ajvFormatOptions, AjvClass, extenderFn } =
46+
options;
47+
this.ajv = createAjvInstance(
48+
additionalMetaSchemas,
49+
customFormats,
50+
ajvOptionsOverrides,
51+
ajvFormatOptions,
52+
AjvClass,
53+
extenderFn,
54+
);
4755
this.localizer = localizer;
4856
}
4957

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,14 @@ describe('compileSchemaValidatorsCode()', () => {
2626
code: { source: true, lines: true },
2727
schemas,
2828
};
29-
expect(createAjvInstance).toHaveBeenCalledWith(undefined, undefined, expectedCompileOpts, undefined, undefined);
29+
expect(createAjvInstance).toHaveBeenCalledWith(
30+
undefined,
31+
undefined,
32+
expectedCompileOpts,
33+
undefined,
34+
undefined,
35+
undefined,
36+
);
3037
});
3138
it('generates the expected output', () => {
3239
expect(generatedCode).toBe(expectedCode);
@@ -53,6 +60,7 @@ describe('compileSchemaValidatorsCode()', () => {
5360
ajvOptionsOverrides = {},
5461
ajvFormatOptions,
5562
AjvClass,
63+
extenderFn,
5664
} = CUSTOM_OPTIONS;
5765
const expectedCompileOpts = {
5866
...ajvOptionsOverrides,
@@ -65,6 +73,7 @@ describe('compileSchemaValidatorsCode()', () => {
6573
expectedCompileOpts,
6674
ajvFormatOptions,
6775
AjvClass,
76+
extenderFn,
6877
);
6978
});
7079
it('generates expected output', () => {

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ import addFormats from 'ajv-formats';
44

55
import createAjvInstance, { AJV_CONFIG, COLOR_FORMAT_REGEX, DATA_URL_FORMAT_REGEX } from '../src/createAjvInstance';
66
import { CUSTOM_OPTIONS } from './harness/testData';
7+
import { CustomValidatorOptionsType } from '../src';
78

89
jest.mock('ajv');
910
jest.mock('ajv/dist/2019');
1011
jest.mock('ajv-formats');
1112

13+
const extender: CustomValidatorOptionsType['extenderFn'] = jest.fn((ajv: Ajv) => ajv);
14+
1215
describe('createAjvInstance()', () => {
1316
describe('no additional meta schemas, custom formats, ajv options overrides or ajv format options', () => {
1417
let ajv: Ajv;
@@ -110,10 +113,10 @@ describe('createAjvInstance()', () => {
110113
expect(ajv.addMetaSchema).toHaveBeenCalledWith(CUSTOM_OPTIONS.additionalMetaSchemas);
111114
});
112115
});
113-
describe('disables ajv format', () => {
116+
describe('disables ajv format and calls extender when provided', () => {
114117
let ajv: Ajv;
115118
beforeAll(() => {
116-
ajv = createAjvInstance(undefined, undefined, undefined, false);
119+
ajv = createAjvInstance(undefined, undefined, undefined, false, undefined, extender);
117120
});
118121
afterAll(() => {
119122
(Ajv as unknown as jest.Mock).mockClear();
@@ -137,5 +140,8 @@ describe('createAjvInstance()', () => {
137140
it('addMetaSchema was not called', () => {
138141
expect(ajv.addMetaSchema).not.toHaveBeenCalled();
139142
});
143+
it('calls the extender when provided', () => {
144+
expect(extender).toHaveBeenCalledWith(ajv);
145+
});
140146
});
141147
});

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Ajv from 'ajv';
12
import { CustomValidatorOptionsType } from '../../src';
23

34
// NOTE these are the same as the CUSTOM_OPTIONS in `compileTestSchema.js`, keep them in sync
@@ -14,4 +15,5 @@ export const CUSTOM_OPTIONS: CustomValidatorOptionsType = {
1415
ajvFormatOptions: {
1516
mode: 'fast',
1617
},
18+
extenderFn: (ajv: Ajv) => ajv,
1719
};

0 commit comments

Comments
 (0)