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

experimental_customMergeAllOf #4308

Merged
Merged
Show file tree
Hide file tree
Changes from 5 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ should change the heading of the (upcoming) version to include a major version b
- Updated `SchemaField` to pass `required` flag to `_AnyOfField`/`_OneOfField`
- Updated `Form` to deal with null objects in `filterErrorsBasedOnSchema()`, fixing [#4306](https://github.com/rjsf-team/react-jsonschema-form/issues/4306)

## @rjsf/utils

- Added `experimental_customMergeAllOf` option to `retrieveSchema` to allow custom merging of `allOf` schemas

## Dev / docs / playground

- Updated the `custom-widgets-fields.md` to add examples of wrapping a widget/field
Expand Down
22 changes: 20 additions & 2 deletions packages/core/src/components/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
validationDataMerge,
ValidatorType,
Experimental_DefaultFormStateBehavior,
Experimental_CustomMergeAllOf,
} from '@rjsf/utils';
import _forEach from 'lodash/forEach';
import _get from 'lodash/get';
Expand Down Expand Up @@ -196,6 +197,9 @@ export interface FormProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
* `emptyObjectFields`
*/
experimental_defaultFormStateBehavior?: Experimental_DefaultFormStateBehavior;
/** Optional function that allows for custom merging of `allOf` schemas
*/
experimental_customMergeAllOf?: Experimental_CustomMergeAllOf<S>;
// Private
/**
* _internalFormWrapper is currently used by the semantic-ui theme to provide a custom wrapper around `<Form />`
Expand Down Expand Up @@ -390,12 +394,26 @@ export default class Form<
'experimental_defaultFormStateBehavior' in props
? props.experimental_defaultFormStateBehavior
: this.props.experimental_defaultFormStateBehavior;
const experimental_customMergeAllOf =
'experimental_customMergeAllOf' in props
? props.experimental_customMergeAllOf
: this.props.experimental_customMergeAllOf;
let schemaUtils: SchemaUtilsType<T, S, F> = state.schemaUtils;
if (
!schemaUtils ||
schemaUtils.doesSchemaUtilsDiffer(props.validator, rootSchema, experimental_defaultFormStateBehavior)
schemaUtils.doesSchemaUtilsDiffer(
props.validator,
rootSchema,
experimental_defaultFormStateBehavior,
experimental_customMergeAllOf
)
) {
schemaUtils = createSchemaUtils<T, S, F>(props.validator, rootSchema, experimental_defaultFormStateBehavior);
schemaUtils = createSchemaUtils<T, S, F>(
props.validator,
rootSchema,
experimental_defaultFormStateBehavior,
experimental_customMergeAllOf
);
}
const formData: T = schemaUtils.getDefaultFormState(schema, inputFormData) as T;
const _retrievedSchema = retrievedSchema ?? schemaUtils.retrieveSchema(schema, formData);
Expand Down
29 changes: 26 additions & 3 deletions packages/docs/docs/api-reference/form-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ The signature and documentation for this property is as follow:
##### computeSkipPopulate <T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>()

A function that determines whether to skip populating the array with default values based on the provided validator, schema, and root schema.
If the function returns `true`, the array will not be populated with default values.
If the function returns `false`, the array will be populated with default values according to the `populate` option.
If the function returns `true`, the array will not be populated with default values.
If the function returns `false`, the array will be populated with default values according to the `populate` option.

###### Parameters

Expand All @@ -104,7 +104,6 @@ A function that determines whether to skip populating the array with default val

- boolean: A boolean indicating whether to skip populating the array with default values.


##### Example

```tsx
Expand Down Expand Up @@ -252,6 +251,30 @@ render(
);
```

## experimental_customMergeAllOf

The `experimental_customMergeAllOf` function allows you to provide a custom implementation for merging `allOf` schemas. This can be particularly useful in scenarios where the default [json-schema-merge-allof](https://github.com/mokkabonna/json-schema-merge-allof) library becomes a performance bottleneck, especially with large and complex schemas or doesn't satisfy your needs.

By providing your own implementation, you can potentially achieve significant performance improvements. For instance, if your use case only requires a subset of JSON Schema features, you can implement a faster, more tailored merging strategy.

If you're looking for alternative `allOf` merging implementations, you might consider [allof-merge](https://github.com/udamir/allof-merge).

**Warning:** This is an experimental feature. Only use this if you fully understand the implications of custom `allOf` merging and are prepared to handle potential edge cases. Incorrect implementations may lead to unexpected behavior or validation errors.

```tsx
import { Form } from '@rjsf/core';
import validator from '@rjsf/validator-ajv8';

const customMergeAllOf = (schema: RJSFSchema): RJSFSchema => {
// Your custom implementation here
};

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

## disabled

It's possible to disable the whole form by setting the `disabled` prop. The `disabled` prop is then forwarded down to each field of the form.
Expand Down
54 changes: 43 additions & 11 deletions packages/utils/src/createSchemaUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import deepEquals from './deepEquals';
import {
ErrorSchema,
Experimental_CustomMergeAllOf,
Experimental_DefaultFormStateBehavior,
FormContextType,
GlobalUISchemaOptions,
Expand Down Expand Up @@ -30,31 +31,36 @@ import {
} from './schema';

/** The `SchemaUtils` class provides a wrapper around the publicly exported APIs in the `utils/schema` directory such
* that one does not have to explicitly pass the `validator`, `rootSchema`, or `experimental_defaultFormStateBehavior` to each method.
* Since these generally do not change across a `Form`, this allows for providing a simplified set of APIs to the
* `@rjsf/core` components and the various themes as well. This class implements the `SchemaUtilsType` interface.
* that one does not have to explicitly pass the `validator`, `rootSchema`, `experimental_defaultFormStateBehavior` or
* `experimental_customMergeAllOf` to each method. Since these generally do not change across a `Form`, this allows for
* providing a simplified set of APIs to the `@rjsf/core` components and the various themes as well. This class
* implements the `SchemaUtilsType` interface.
*/
class SchemaUtils<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>
implements SchemaUtilsType<T, S, F>
{
rootSchema: S;
validator: ValidatorType<T, S, F>;
experimental_defaultFormStateBehavior: Experimental_DefaultFormStateBehavior;
experimental_customMergeAllOf?: Experimental_CustomMergeAllOf<S>;

/** Constructs the `SchemaUtils` instance with the given `validator` and `rootSchema` stored as instance variables
*
* @param validator - An implementation of the `ValidatorType` interface that will be forwarded to all the APIs
* @param rootSchema - The root schema that will be forwarded to all the APIs
* @param experimental_defaultFormStateBehavior - Configuration flags to allow users to override default form state behavior
* @param [experimental_customMergeAllOf] - Optional function that allows for custom merging of `allOf` schemas
*/
constructor(
validator: ValidatorType<T, S, F>,
rootSchema: S,
experimental_defaultFormStateBehavior: Experimental_DefaultFormStateBehavior
experimental_defaultFormStateBehavior: Experimental_DefaultFormStateBehavior,
experimental_customMergeAllOf?: Experimental_CustomMergeAllOf<S>
) {
this.rootSchema = rootSchema;
this.validator = validator;
this.experimental_defaultFormStateBehavior = experimental_defaultFormStateBehavior;
this.experimental_customMergeAllOf = experimental_customMergeAllOf;
}

/** Returns the `ValidatorType` in the `SchemaUtilsType`
Expand All @@ -72,20 +78,23 @@ class SchemaUtils<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
* @param validator - An implementation of the `ValidatorType` interface that will be compared against the current one
* @param rootSchema - The root schema that will be compared against the current one
* @param [experimental_defaultFormStateBehavior] Optional configuration object, if provided, allows users to override default form state behavior
* @param [experimental_customMergeAllOf] - Optional function that allows for custom merging of `allOf` schemas
* @returns - True if the `SchemaUtilsType` differs from the given `validator` or `rootSchema`
*/
doesSchemaUtilsDiffer(
validator: ValidatorType<T, S, F>,
rootSchema: S,
experimental_defaultFormStateBehavior = {}
experimental_defaultFormStateBehavior = {},
experimental_customMergeAllOf?: Experimental_CustomMergeAllOf<S>
): boolean {
if (!validator || !rootSchema) {
return false;
}
return (
this.validator !== validator ||
!deepEquals(this.rootSchema, rootSchema) ||
!deepEquals(this.experimental_defaultFormStateBehavior, experimental_defaultFormStateBehavior)
!deepEquals(this.experimental_defaultFormStateBehavior, experimental_defaultFormStateBehavior) ||
this.experimental_customMergeAllOf !== experimental_customMergeAllOf
);
}

Expand All @@ -110,7 +119,8 @@ class SchemaUtils<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
formData,
this.rootSchema,
includeUndefinedValues,
this.experimental_defaultFormStateBehavior
this.experimental_defaultFormStateBehavior,
this.experimental_customMergeAllOf
);
}

Expand Down Expand Up @@ -234,7 +244,13 @@ class SchemaUtils<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
* @returns - The schema having its conditions, additional properties, references and dependencies resolved
*/
retrieveSchema(schema: S, rawFormData?: T) {
return retrieveSchema<T, S, F>(this.validator, schema, this.rootSchema, rawFormData);
return retrieveSchema<T, S, F>(
this.validator,
schema,
this.rootSchema,
rawFormData,
this.experimental_customMergeAllOf
);
}

/** Sanitize the `data` associated with the `oldSchema` so it is considered appropriate for the `newSchema`. If the
Expand Down Expand Up @@ -262,7 +278,16 @@ class SchemaUtils<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
* @returns - The `IdSchema` object for the `schema`
*/
toIdSchema(schema: S, id?: string | null, formData?: T, idPrefix = 'root', idSeparator = '_'): IdSchema<T> {
return toIdSchema<T, S, F>(this.validator, schema, id, this.rootSchema, formData, idPrefix, idSeparator);
return toIdSchema<T, S, F>(
this.validator,
schema,
id,
this.rootSchema,
formData,
idPrefix,
idSeparator,
this.experimental_customMergeAllOf
);
}

/** Generates an `PathSchema` object for the `schema`, recursively
Expand All @@ -283,6 +308,7 @@ class SchemaUtils<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
* @param validator - an implementation of the `ValidatorType` interface that will be forwarded to all the APIs
* @param rootSchema - The root schema that will be forwarded to all the APIs
* @param [experimental_defaultFormStateBehavior] Optional configuration object, if provided, allows users to override default form state behavior
* @param [experimental_customMergeAllOf] - Optional function that allows for custom merging of `allOf` schemas
* @returns - An implementation of a `SchemaUtilsType` interface
*/
export default function createSchemaUtils<
Expand All @@ -292,7 +318,13 @@ export default function createSchemaUtils<
>(
validator: ValidatorType<T, S, F>,
rootSchema: S,
experimental_defaultFormStateBehavior = {}
experimental_defaultFormStateBehavior = {},
experimental_customMergeAllOf?: Experimental_CustomMergeAllOf<S>
): SchemaUtilsType<T, S, F> {
return new SchemaUtils<T, S, F>(validator, rootSchema, experimental_defaultFormStateBehavior);
return new SchemaUtils<T, S, F>(
validator,
rootSchema,
experimental_defaultFormStateBehavior,
experimental_customMergeAllOf
);
}
25 changes: 21 additions & 4 deletions packages/utils/src/schema/getDefaultFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import mergeDefaultsWithFormData from '../mergeDefaultsWithFormData';
import mergeObjects from '../mergeObjects';
import mergeSchemas from '../mergeSchemas';
import {
Experimental_CustomMergeAllOf,
Experimental_DefaultFormStateBehavior,
FormContextType,
GenericObjectType,
Expand Down Expand Up @@ -156,6 +157,8 @@ interface ComputeDefaultsProps<T = any, S extends StrictRJSFSchema = RJSFSchema>
_recurseList?: string[];
/** Optional configuration object, if provided, allows users to override default form state behavior */
experimental_defaultFormStateBehavior?: Experimental_DefaultFormStateBehavior;
/** Optional function that allows for custom merging of `allOf` schemas */
experimental_customMergeAllOf?: Experimental_CustomMergeAllOf<S>;
/** Optional flag, if true, indicates this schema was required in the parent schema. */
required?: boolean;
}
Expand All @@ -180,6 +183,7 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
includeUndefinedValues = false,
_recurseList = [],
experimental_defaultFormStateBehavior = undefined,
experimental_customMergeAllOf = undefined,
required,
} = computeDefaultsProps;
const formData: T = (isObject(rawFormData) ? rawFormData : {}) as T;
Expand Down Expand Up @@ -209,7 +213,15 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
...formData,
...getDefaultBasedOnSchemaType(validator, schema, computeDefaultsProps, defaults),
};
const resolvedSchema = resolveDependencies<T, S, F>(validator, schema, rootSchema, false, [], defaultFormData);
const resolvedSchema = resolveDependencies<T, S, F>(
validator,
schema,
rootSchema,
false,
[],
defaultFormData,
experimental_customMergeAllOf
);
schemaToCompute = resolvedSchema[0]; // pick the first element from resolve dependencies
} else if (isFixedItems(schema)) {
defaults = (schema.items! as S[]).map((itemSchema: S, idx: number) =>
Expand Down Expand Up @@ -298,6 +310,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
includeUndefinedValues = false,
_recurseList = [],
experimental_defaultFormStateBehavior = undefined,
experimental_customMergeAllOf = undefined,
required,
}: ComputeDefaultsProps<T, S> = {},
defaults?: T | T[] | undefined
Expand All @@ -309,7 +322,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
// https://github.com/rjsf-team/react-jsonschema-form/issues/3832
const retrievedSchema =
experimental_defaultFormStateBehavior?.allOf === 'populateDefaults' && ALL_OF_KEY in schema
? retrieveSchema<T, S, F>(validator, schema, rootSchema, formData)
? retrieveSchema<T, S, F>(validator, schema, rootSchema, formData, experimental_customMergeAllOf)
: schema;
const objectDefaults = Object.keys(retrievedSchema.properties || {}).reduce(
(acc: GenericObjectType, key: string) => {
Expand All @@ -319,6 +332,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
rootSchema,
_recurseList,
experimental_defaultFormStateBehavior,
experimental_customMergeAllOf,
includeUndefinedValues: includeUndefinedValues === true,
parentDefaults: get(defaults, [key]),
rawFormData: get(formData, [key]),
Expand Down Expand Up @@ -521,6 +535,7 @@ export function getDefaultBasedOnSchemaType<
* If "excludeObjectChildren", cause undefined values for this object and pass `includeUndefinedValues` as
* false when computing defaults for any nested object properties.
* @param [experimental_defaultFormStateBehavior] Optional configuration object, if provided, allows users to override default form state behavior
* @param [experimental_customMergeAllOf] - Optional function that allows for custom merging of `allOf` schemas
* @returns - The resulting `formData` with all the defaults provided
*/
export default function getDefaultFormState<
Expand All @@ -533,16 +548,18 @@ export default function getDefaultFormState<
formData?: T,
rootSchema?: S,
includeUndefinedValues: boolean | 'excludeObjectChildren' = false,
experimental_defaultFormStateBehavior?: Experimental_DefaultFormStateBehavior
experimental_defaultFormStateBehavior?: Experimental_DefaultFormStateBehavior,
experimental_customMergeAllOf?: Experimental_CustomMergeAllOf<S>
) {
if (!isObject(theSchema)) {
throw new Error('Invalid schema: ' + theSchema);
}
const schema = retrieveSchema<T, S, F>(validator, theSchema, rootSchema, formData);
const schema = retrieveSchema<T, S, F>(validator, theSchema, rootSchema, formData, experimental_customMergeAllOf);
const defaults = computeDefaults<T, S, F>(validator, schema, {
rootSchema,
includeUndefinedValues,
experimental_defaultFormStateBehavior,
experimental_customMergeAllOf,
rawFormData: formData,
});
if (formData === undefined || formData === null || (typeof formData === 'number' && isNaN(formData))) {
Expand Down
Loading