@rjsf/utils
Version:
Utility functions for @rjsf/core
158 lines (139 loc) • 6.28 kB
text/typescript
import {
ADDITIONAL_PROPERTIES_KEY,
ALL_OF_KEY,
ANY_OF_KEY,
ITEMS_KEY,
ONE_OF_KEY,
PROPERTIES_KEY,
REF_KEY,
RJSF_REF_KEY,
} from './constants';
import findSchemaDefinition from './findSchemaDefinition';
import isObject from './isObject';
import mergeObjects from './mergeObjects';
import { FormContextType, GenericObjectType, Registry, RJSFSchema, StrictRJSFSchema, UiSchema } from './types';
// Keywords where child schemas map to uiSchema at the SAME key
const SAME_KEY_KEYWORDS = [ITEMS_KEY, ADDITIONAL_PROPERTIES_KEY] as const;
// Keywords where child schemas are in an array, each mapping to uiSchema[keyword][i]
const ARRAY_KEYWORDS = [ONE_OF_KEY, ANY_OF_KEY, ALL_OF_KEY] as const;
/** Expands `ui:definitions` into the uiSchema by walking the schema tree and finding all `$ref`s.
* Called once at form initialization to pre-expand definitions into the uiSchema structure.
*
* For recursive schemas, expansion stops at recursion points to avoid infinite loops.
* Runtime resolution via `resolveUiSchema` handles these cases using registry definitions.
*
* @param currentSchema - The current schema node being processed
* @param uiSchema - The uiSchema at the current path
* @param registry - The registry containing rootSchema and uiSchemaDefinitions
* @param visited - Set of $refs already visited (to detect recursion)
* @returns - The expanded uiSchema with definitions merged in
*/
export function expandUiSchemaDefinitions<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any,
>(
currentSchema: S,
uiSchema: UiSchema<T, S, F>,
registry: Registry<T, S, F>,
visited: Set<string> = new Set(),
): UiSchema<T, S, F> {
const { rootSchema, uiSchemaDefinitions: definitions } = registry;
let result = { ...uiSchema };
let resolvedSchema = currentSchema;
const ref = currentSchema[REF_KEY] as string | undefined;
const isRecursive = ref && visited.has(ref);
if (ref) {
visited.add(ref);
if (definitions && ref in definitions) {
result = mergeObjects(definitions[ref] as GenericObjectType, result as GenericObjectType) as UiSchema<T, S, F>;
}
if (isRecursive) {
return result;
}
try {
resolvedSchema = findSchemaDefinition<S>(ref, rootSchema as S);
} catch {
resolvedSchema = currentSchema;
}
}
// Process properties (each property maps to uiSchema[propName] - flattened)
const properties = resolvedSchema[PROPERTIES_KEY];
if (properties && isObject(properties)) {
for (const [propName, propSchema] of Object.entries(properties as Record<string, S>)) {
const propUiSchema = (result[propName] || {}) as UiSchema<T, S, F>;
const expanded = expandUiSchemaDefinitions(propSchema, propUiSchema, registry, new Set(visited));
if (Object.keys(expanded).length > 0) {
result[propName] = expanded;
}
}
}
// Process keywords where child maps to same key in uiSchema (items, additionalProperties)
for (const keyword of SAME_KEY_KEYWORDS) {
const subSchema = resolvedSchema[keyword];
if (subSchema && isObject(subSchema) && !Array.isArray(subSchema)) {
const currentUiSchema = result[keyword];
if (typeof currentUiSchema !== 'function') {
const subUiSchema = ((currentUiSchema as GenericObjectType) || {}) as UiSchema<T, S, F>;
const expanded = expandUiSchemaDefinitions(subSchema as S, subUiSchema, registry, new Set(visited));
if (Object.keys(expanded).length > 0) {
(result as GenericObjectType)[keyword] = expanded;
}
}
}
}
// Process array keywords (oneOf, anyOf, allOf) - each option maps to uiSchema[keyword][i]
for (const keyword of ARRAY_KEYWORDS) {
const schemaOptions = resolvedSchema[keyword];
if (Array.isArray(schemaOptions) && schemaOptions.length > 0) {
const currentUiSchemaArray = (result as GenericObjectType)[keyword];
const uiSchemaArray: UiSchema<T, S, F>[] = Array.isArray(currentUiSchemaArray) ? [...currentUiSchemaArray] : [];
let hasExpanded = false;
for (let i = 0; i < schemaOptions.length; i++) {
const optionSchema = schemaOptions[i] as S;
const optionUiSchema = (uiSchemaArray[i] || {}) as UiSchema<T, S, F>;
const expanded = expandUiSchemaDefinitions(optionSchema, optionUiSchema, registry, new Set(visited));
if (Object.keys(expanded).length > 0) {
uiSchemaArray[i] = expanded;
hasExpanded = true;
}
}
if (hasExpanded) {
(result as GenericObjectType)[keyword] = uiSchemaArray;
}
}
}
return result;
}
/** Resolves the uiSchema for a given schema, considering `ui:definitions` stored in the registry.
*
* This function is called at runtime for each field. It handles recursive schemas where the
* pre-expansion in `expandUiSchemaDefinitions` couldn't go deeper.
*
* When the schema contains a `$ref`, this function looks up the corresponding uiSchema definition
* from `registry.uiSchemaDefinitions` and merges it with any local uiSchema overrides.
*
* Resolution order (later sources override earlier):
* 1. `ui:definitions[$ref]` - base definition from registry
* 2. `localUiSchema` - local overrides at current path
*
* @param schema - The JSON schema (may still contain `$ref` for recursive schemas)
* @param localUiSchema - The uiSchema at the current path (local overrides)
* @param registry - The registry containing `uiSchemaDefinitions`
* @returns - The resolved uiSchema with definitions merged in
*/
export default function resolveUiSchema<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any,
>(schema: S, localUiSchema: UiSchema<T, S, F> | undefined, registry: Registry<T, S, F>): UiSchema<T, S, F> {
const ref = ((schema as GenericObjectType)[RJSF_REF_KEY] ?? schema[REF_KEY]) as string | undefined;
const definitionUiSchema = ref ? registry.uiSchemaDefinitions?.[ref] : undefined;
if (!definitionUiSchema) {
return localUiSchema || {};
}
if (!localUiSchema || Object.keys(localUiSchema).length === 0) {
return { ...definitionUiSchema };
}
return mergeObjects(definitionUiSchema as GenericObjectType, localUiSchema as GenericObjectType) as UiSchema<T, S, F>;
}