Skip to content
Merged
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ should change the heading of the (upcoming) version to include a major version b

- 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))

## @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, fixing [#4746](https://github.com/rjsf-team/react-jsonschema-form/issues/4746)
- 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, fixing [#4167](https://github.com/rjsf-team/react-jsonschema-form/issues/4167)

## Dev / docs / playground

- Updated `DemoFrame` as follows to fix [#3609](https://github.com/rjsf-team/react-jsonschema-form/issues/3609)
Expand All @@ -40,6 +47,7 @@ should change the heading of the (upcoming) version to include a major version b
- Update the `antd` theme wrapper to render the `AntdSelectPatcher`, `AntdStyleProvider` and `ConfigProvider` with it's own `getPopupContainer()` function inside of a `FrameContextConsumer`
- Updated the base TypeScript configuration to use `"moduleResolution": "bundler"`
- 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)
- Also added documentation for the new `extenderFn` prop on `CustomValidatorOptionsType`

# 6.1.1

Expand Down
20 changes: 20 additions & 0 deletions packages/docs/docs/usage/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,26 @@ const validator = customizeValidator({ AjvClass: Ajv2019 });
render(<Form schema={schema} validator={validator} />, document.getElementById('app'));
```

### extenderFn

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.

```tsx
import { Form } from '@rjsf/core';
import { RJSFSchema } from '@rjsf/utils';
import { customizeValidator } from '@rjsf/validator-ajv8';
import ajvErrors from 'ajv-errors';

const schema: RJSFSchema = {
type: 'string',
format: 'date',
};

const validator = customizeValidator({ extenderFn: ajvErrors });

render(<Form schema={schema} validator={validator} />, document.getElementById('app'));
```

### Localization (L10n) support

The Ajv 8 validator supports the localization of error messages using [ajv-i18n](https://github.com/ajv-validator/ajv-i18n).
Expand Down
18 changes: 16 additions & 2 deletions packages/validator-ajv8/src/compileSchemaValidatorsCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,28 @@ export function compileSchemaValidatorsCode<S extends StrictRJSFSchema = RJSFSch
const schemaMaps = schemaParser(schema);
const schemas = Object.values(schemaMaps);

const { additionalMetaSchemas, customFormats, ajvOptionsOverrides = {}, ajvFormatOptions, AjvClass } = options;
const {
additionalMetaSchemas,
customFormats,
ajvOptionsOverrides = {},
ajvFormatOptions,
AjvClass,
extenderFn,
} = options;
// Allow users to turn off the `lines: true` feature in their own overrides, but NOT the `source: true`
const compileOptions = {
...ajvOptionsOverrides,
code: { lines: true, ...ajvOptionsOverrides.code, source: true },
schemas,
};
const ajv = createAjvInstance(additionalMetaSchemas, customFormats, compileOptions, ajvFormatOptions, AjvClass);
const ajv = createAjvInstance(
additionalMetaSchemas,
customFormats,
compileOptions,
ajvFormatOptions,
AjvClass,
extenderFn,
);

return standaloneCode(ajv);
}
7 changes: 6 additions & 1 deletion packages/validator-ajv8/src/createAjvInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,17 @@ export const DATA_URL_FORMAT_REGEX = /^data:([a-z]+\/[a-z0-9-+.]+)?;(?:name=(.*)
* @param [ajvOptionsOverrides={}] - The set of validator config override options
* @param [ajvFormatOptions] - The `ajv-format` options to use when adding formats to `ajv`; pass `false` to disable it
* @param [AjvClass] - The `Ajv` class to use when creating the validator instance
* @param [extenderFn] - A function to call to extend AJV, such as `ajvErrors()`
*/
export default function createAjvInstance(
additionalMetaSchemas?: CustomValidatorOptionsType['additionalMetaSchemas'],
customFormats?: CustomValidatorOptionsType['customFormats'],
ajvOptionsOverrides: CustomValidatorOptionsType['ajvOptionsOverrides'] = {},
ajvFormatOptions?: FormatsPluginOptions | false,
AjvClass: typeof Ajv = Ajv,
extenderFn?: CustomValidatorOptionsType['extenderFn'],
) {
const ajv = new AjvClass({ ...AJV_CONFIG, ...ajvOptionsOverrides });
let ajv = new AjvClass({ ...AJV_CONFIG, ...ajvOptionsOverrides });
if (ajvFormatOptions) {
addFormats(ajv, ajvFormatOptions);
} else if (ajvFormatOptions !== false) {
Expand All @@ -64,6 +66,9 @@ export default function createAjvInstance(
ajv.addFormat(formatName, customFormats[formatName]);
});
}
if (extenderFn) {
ajv = extenderFn(ajv);
}

return ajv;
}
26 changes: 25 additions & 1 deletion packages/validator-ajv8/src/processRawValidationErrors.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { ErrorObject } from 'ajv';
import get from 'lodash/get';
import {
ANY_OF_KEY,
createErrorHandler,
CustomValidator,
ErrorTransformer,
FormContextType,
getDefaultFormState,
getUiOptions,
ONE_OF_KEY,
PROPERTIES_KEY,
RJSFSchema,
RJSFValidationError,
Expand Down Expand Up @@ -34,7 +36,7 @@ export function transformRJSFValidationErrors<
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any,
>(errors: ErrorObject[] = [], uiSchema?: UiSchema<T, S, F>): RJSFValidationError[] {
return errors.map((e: ErrorObject) => {
const errorList = errors.map((e: ErrorObject) => {
const { instancePath, keyword, params, schemaPath, parentSchema, ...rest } = e;
let { message = '' } = rest;
let property = instancePath.replace(/\//g, '.');
Expand Down Expand Up @@ -105,6 +107,28 @@ export function transformRJSFValidationErrors<
title: uiTitle,
};
});
// Filter out duplicates around anyOf/oneOf messages
return errorList.reduce((acc: RJSFValidationError[], err: RJSFValidationError) => {
const { message, schemaPath } = err;
const anyOfIndex = schemaPath?.indexOf(`/${ANY_OF_KEY}/`);
const oneOfIndex = schemaPath?.indexOf(`/${ONE_OF_KEY}/`);
let schemaPrefix: string | undefined;
// Look specifically for `/anyOr/` or `/oneOf/` within the schemaPath information
if (anyOfIndex && anyOfIndex >= 0) {
schemaPrefix = schemaPath?.substring(0, anyOfIndex);
} else if (oneOfIndex && oneOfIndex >= 0) {
schemaPrefix = schemaPath?.substring(0, oneOfIndex);
}
// If there is a schemaPrefix, then search for a duplicate message with the same prefix, otherwise undefined
const dup = schemaPrefix
? acc.find((e: RJSFValidationError) => e.message === message && e.schemaPath?.startsWith(schemaPrefix))
: undefined;
if (!dup) {
// Only push an error that is not a duplicate
acc.push(err);
}
return acc;
}, [] as RJSFValidationError[]);
}

/** This function processes the `formData` with an optional user contributed `customValidate` function, which receives
Expand Down
2 changes: 2 additions & 0 deletions packages/validator-ajv8/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export interface CustomValidatorOptionsType {
ajvFormatOptions?: FormatsPluginOptions | false;
/** The AJV class to construct */
AjvClass?: typeof Ajv;
/** A function to call to extend AJV, such as `ajvErrors()` */
extenderFn?: (ajv: Ajv) => Ajv;
}

/** The type describing a function that takes a list of Ajv `ErrorObject`s and localizes them
Expand Down
12 changes: 10 additions & 2 deletions packages/validator-ajv8/src/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,16 @@ export default class AJV8Validator<T = any, S extends StrictRJSFSchema = RJSFSch
* @param [localizer] - If provided, is used to localize a list of Ajv `ErrorObject`s
*/
constructor(options: CustomValidatorOptionsType, localizer?: Localizer) {
const { additionalMetaSchemas, customFormats, ajvOptionsOverrides, ajvFormatOptions, AjvClass } = options;
this.ajv = createAjvInstance(additionalMetaSchemas, customFormats, ajvOptionsOverrides, ajvFormatOptions, AjvClass);
const { additionalMetaSchemas, customFormats, ajvOptionsOverrides, ajvFormatOptions, AjvClass, extenderFn } =
options;
this.ajv = createAjvInstance(
additionalMetaSchemas,
customFormats,
ajvOptionsOverrides,
ajvFormatOptions,
AjvClass,
extenderFn,
);
this.localizer = localizer;
}

Expand Down
11 changes: 10 additions & 1 deletion packages/validator-ajv8/test/compileSchemaValidatorsCode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ describe('compileSchemaValidatorsCode()', () => {
code: { source: true, lines: true },
schemas,
};
expect(createAjvInstance).toHaveBeenCalledWith(undefined, undefined, expectedCompileOpts, undefined, undefined);
expect(createAjvInstance).toHaveBeenCalledWith(
undefined,
undefined,
expectedCompileOpts,
undefined,
undefined,
undefined,
);
});
it('generates the expected output', () => {
expect(generatedCode).toBe(expectedCode);
Expand All @@ -53,6 +60,7 @@ describe('compileSchemaValidatorsCode()', () => {
ajvOptionsOverrides = {},
ajvFormatOptions,
AjvClass,
extenderFn,
} = CUSTOM_OPTIONS;
const expectedCompileOpts = {
...ajvOptionsOverrides,
Expand All @@ -65,6 +73,7 @@ describe('compileSchemaValidatorsCode()', () => {
expectedCompileOpts,
ajvFormatOptions,
AjvClass,
extenderFn,
);
});
it('generates expected output', () => {
Expand Down
10 changes: 8 additions & 2 deletions packages/validator-ajv8/test/createAjvInstance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import addFormats from 'ajv-formats';

import createAjvInstance, { AJV_CONFIG, COLOR_FORMAT_REGEX, DATA_URL_FORMAT_REGEX } from '../src/createAjvInstance';
import { CUSTOM_OPTIONS } from './harness/testData';
import { CustomValidatorOptionsType } from '../src';

jest.mock('ajv');
jest.mock('ajv/dist/2019');
jest.mock('ajv-formats');

const extender: CustomValidatorOptionsType['extenderFn'] = jest.fn((ajv: Ajv) => ajv);

describe('createAjvInstance()', () => {
describe('no additional meta schemas, custom formats, ajv options overrides or ajv format options', () => {
let ajv: Ajv;
Expand Down Expand Up @@ -110,10 +113,10 @@ describe('createAjvInstance()', () => {
expect(ajv.addMetaSchema).toHaveBeenCalledWith(CUSTOM_OPTIONS.additionalMetaSchemas);
});
});
describe('disables ajv format', () => {
describe('disables ajv format and calls extender when provided', () => {
let ajv: Ajv;
beforeAll(() => {
ajv = createAjvInstance(undefined, undefined, undefined, false);
ajv = createAjvInstance(undefined, undefined, undefined, false, undefined, extender);
});
afterAll(() => {
(Ajv as unknown as jest.Mock).mockClear();
Expand All @@ -137,5 +140,8 @@ describe('createAjvInstance()', () => {
it('addMetaSchema was not called', () => {
expect(ajv.addMetaSchema).not.toHaveBeenCalled();
});
it('calls the extender when provided', () => {
expect(extender).toHaveBeenCalledWith(ajv);
});
});
});
2 changes: 2 additions & 0 deletions packages/validator-ajv8/test/harness/testData.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Ajv from 'ajv';
import { CustomValidatorOptionsType } from '../../src';

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