@rjsf/utils
Version:
Utility functions for @rjsf/core
418 lines • 22.5 kB
JavaScript
import get from 'lodash-es/get.js';
import isEmpty from 'lodash-es/isEmpty.js';
import isNil from 'lodash-es/isNil.js';
import pick from 'lodash-es/pick.js';
import { NAME_KEY, RJSF_ADDITIONAL_PROPERTIES_FLAG } from '../constants.js';
import findSchemaDefinition from '../findSchemaDefinition.js';
import getDiscriminatorFieldFromSchema from '../getDiscriminatorFieldFromSchema.js';
import getSchemaType from '../getSchemaType.js';
import isObject from '../isObject.js';
import getClosestMatchingOption from './getClosestMatchingOption.js';
import isSelect from './isSelect.js';
import { relaxOptionsForScoring, resolveAllReferences } from './retrieveSchema.js';
import shallowAllOfMerge from './shallowAllOfMerge.js';
/** Returns the `formData` with only the elements specified in the `fields` list
*
* @param formData - The data for the `Form`
* @param fields - The fields to keep while filtering
* @deprecated - To be removed as an exported `@rjsf/utils` function in a future release
*/
export function getUsedFormData(formData, fields) {
// For the case of a single input form
if (fields.length === 0 && typeof formData !== 'object') {
return formData;
}
const data = pick(formData, fields);
if (Array.isArray(formData)) {
return Object.keys(data).map((key) => data[key]);
}
return data;
}
/** Returns the list of field names from inspecting the `pathSchema` as well as using the `formData`
*
* @param pathSchema - The `PathSchema` object for the form
* @param [formData] - The form data to use while checking for empty objects/arrays
* @deprecated - To be removed as an exported `@rjsf/utils` function in a future release
*/
// oxlint-disable-next-line typescript/no-deprecated
export function getFieldNames(pathSchema, formData) {
const formValueHasData = (value, isLeaf) => typeof value !== 'object' || isEmpty(value) || (isLeaf && !isEmpty(value));
const getAllPaths = (_obj, acc = [], paths = [[]]) => {
const objKeys = Object.keys(_obj);
objKeys.forEach((key) => {
const data = _obj[key];
if (typeof data === 'object') {
const newPaths = paths.map((path) => [...path, key]);
// If an object is marked with additionalProperties, all its keys are valid
if (data[RJSF_ADDITIONAL_PROPERTIES_FLAG] && data[NAME_KEY] !== '') {
acc.push(data[NAME_KEY]);
}
else {
getAllPaths(data, acc, newPaths);
}
}
else if (key === NAME_KEY && data !== '') {
paths.forEach((path) => {
const formValue = get(formData, path);
const isLeaf = objKeys.length === 1;
// adds path to fieldNames if it points to a value or an empty object/array which is not a leaf
if (formValueHasData(formValue, isLeaf) ||
(Array.isArray(formValue) && formValue.every((val) => formValueHasData(val, isLeaf)))) {
acc.push(path);
}
});
}
});
return acc;
};
return getAllPaths(pathSchema);
}
/** Returns true when a form value is considered empty: null/undefined/'', an empty array, or a plain
* object whose every own value is itself empty (recursive). Scalars like `0` and `false` are not empty.
*
* @param value - The value to check
* @returns - True if the value is considered empty, false otherwise
*/
export function isValueEmpty(value) {
if (isNil(value) || value === '') {
return true;
}
if (Array.isArray(value)) {
return value.length === 0;
}
if (isObject(value)) {
return Object.values(value).every(isValueEmpty);
}
return false;
}
/** Merges an `allOf` schema into a single flat schema, delegating to `experimental_customMergeAllOf`
* when provided or falling back to the module-level `shallowAllOfMerge` otherwise.
*
* @param schema - A schema containing an `allOf` array to be merged
* @param [experimental_customMergeAllOf] - Optional custom merge function; see `Form` documentation
* @returns - The merged schema with `allOf` resolved into a single schema object
*/
function doMergeAllOf(schema, experimental_customMergeAllOf) {
return experimental_customMergeAllOf ? experimental_customMergeAllOf(schema) : shallowAllOfMerge(schema);
}
/** A recursive, schema-driven filter that walks `schema` and `formData` in lockstep, keeping only
* values that are described by the schema. Handles `$ref`, `allOf`, `anyOf`, `oneOf`, `if/then/else`,
* `patternProperties`, `additionalProperties`, `propertyNames`, and `dependencies`. Optional object
* properties whose schema-filtered content is entirely empty (per `isValueEmpty`) are pruned; required
* properties and scalar values are always kept when schema-defined.
*
* @param validator - An implementation of the `ValidatorType` interface that will be used when necessary
* @param schema - The schema for which to filter the formData
* @param [rootSchema] - The root schema, used primarily to look up `$ref`s
* @param [formData] - The data for the `Form`
* @param [experimental_customMergeAllOf] - Optional function that allows for custom merging of `allOf` schemas
* @returns - The `formData` after omitting extra data, or `undefined` when `formData` is undefined
*/
export default function omitExtraData(validator, schema, rootSchema = {}, formData, experimental_customMergeAllOf) {
/** Type predicate that narrows `value` to `GenericObjectType` — true when `value` is a plain,
* non-array object (i.e. a JSON object). Used to distinguish JSON objects from arrays and primitives.
*
* @param value - The value to check
* @returns - True if `value` is a plain non-array object
*/
function isObjectValue(value) {
return isObject(value);
}
/** Type predicate that narrows a `S | boolean` schema definition to `S` — true when `schemaDef` is
* a schema object rather than a JSON Schema boolean shorthand (`true` meaning allow-all, `false`
* meaning deny-all).
*
* @param schemaDef - The schema definition to check
* @returns - True if `schemaDef` is a schema object
*/
function isSchemaObj(schemaDef) {
return isObject(schemaDef);
}
/** Copies schema-defined properties from `source` into `target`, applying `omit` recursively for
* each value. Handles `properties`, `patternProperties`, `additionalProperties`, and `propertyNames`.
* Optional object-valued properties are pruned when every key in the filtered result is both
* optional (per the inner schema's `required`) and empty (per `isValueEmpty`). This preserves
* optional objects whose required children have empty values, while still dropping objects whose
* schema-filtered content is entirely optional-and-empty. Required properties and scalar values
* are always written when defined. Always returns `target` — pruning of the object itself is
* the caller's responsibility.
*
* @param childSchema - The object schema describing which properties are allowed
* @param source - The source form data object to read values from
* @param target - The accumulator object to write filtered values into
* @returns - `target` after all schema-defined properties have been processed
*/
function handleObject(childSchema, source, target) {
var _a;
const { properties, additionalProperties, patternProperties, propertyNames } = childSchema;
const requiredSet = new Set((_a = childSchema.required) !== null && _a !== void 0 ? _a : []);
/** Recursively omits extra data from `value` via `omit`, then conditionally writes the result to
* `target[key]`. Optional object-valued properties are dropped when every key in the filtered
* result is both optional (within the inner schema's `required` list) and has an empty value per
* `isValueEmpty`. This per-key approach prevents required-but-empty child properties — kept by
* inner `setProperty` calls — from inadvertently causing the parent optional object to be dropped,
* while still pruning optional objects whose schema-filtered content is entirely empty. Vacuously
* true for `{}`, so empty results are always dropped. Scalar and array values are not pruned here.
*
* @param key - The property key to write on `target`
* @param schemaDef - The schema (or boolean shorthand) that governs the value at `key`
* @param value - The raw source value to filter
* @param [required=false] - When true the property is never pruned regardless of its filtered value
*/
function setProperty(key, schemaDef, value, required = false) {
var _a;
const v = omit(schemaDef, value, target[key]);
if (!required && isObject(v)) {
// Resolve $ref so we can inspect the effective required list for the inner schema.
let sd = isSchemaObj(schemaDef) ? schemaDef : {};
if (sd.$ref !== undefined) {
sd = findSchemaDefinition(sd.$ref, rootSchema);
}
const innerRequired = new Set((_a = sd.required) !== null && _a !== void 0 ? _a : []);
// Drop this optional object when every key in v is both optional in the inner schema
// and has an empty value. Vacuously true for {} so empty objects are always dropped.
const shouldDrop = Object.entries(v).every(([k, val]) => !innerRequired.has(k) && isValueEmpty(val));
if (shouldDrop) {
return;
}
}
if (v !== undefined) {
// oxlint-disable-next-line no-param-reassign
target[key] = v;
}
}
if (properties !== undefined) {
for (const [key, schemaDef] of Object.entries(properties)) {
setProperty(key, schemaDef, source[key], requiredSet.has(key));
}
}
// Track keys not handled by properties/patterns so additionalProperties can pick them up.
let patternPropertiesRest;
if (patternProperties !== undefined) {
patternPropertiesRest = [];
const patterns = Object.entries(patternProperties).map(([pattern, schemaDef]) => [
new RegExp(pattern),
schemaDef,
]);
const knownProperties = new Set(Object.keys(properties !== null && properties !== void 0 ? properties : {}));
for (const [key, value] of Object.entries(source)) {
if (!knownProperties.has(key)) {
const matched = patterns.find(([re]) => re.test(key));
if (matched === undefined) {
patternPropertiesRest.push(key);
}
else {
setProperty(key, matched[1], value);
}
}
}
}
// JSON Schema spec: absent additionalProperties defaults to true (allow all extra keys). Here
// we treat it as false so omitExtraData never inadvertently passes through undescribed keys.
if (additionalProperties !== undefined && additionalProperties !== false) {
const addlSchema = additionalProperties;
if (patternPropertiesRest !== undefined) {
for (const key of patternPropertiesRest) {
setProperty(key, addlSchema, source[key]);
}
}
else {
const knownProperties = new Set(Object.keys(properties !== null && properties !== void 0 ? properties : {}));
for (const [key, value] of Object.entries(source)) {
if (!knownProperties.has(key)) {
setProperty(key, addlSchema, value);
}
}
}
}
// When propertyNames is present, the schema only constrains key names — all source keys are valid.
if (propertyNames !== undefined) {
for (const [key, value] of Object.entries(source)) {
// oxlint-disable-next-line no-param-reassign
target[key] = value;
}
}
return target;
}
/** Filters array elements from `source` into `target` according to `schema.items` and
* `schema.additionalItems`. For tuple schemas (`items` is an array) each element is filtered by its
* per-index schema; elements beyond the tuple length are covered by `additionalItems` when present.
* For list schemas (`items` is a single schema) every element is filtered by that schema.
*
* @param childSchema - The array schema describing `items` and optionally `additionalItems`
* @param source - The source array to read elements from
* @param target - The accumulator array to push filtered elements into
* @returns - `target` after all applicable source elements have been pushed
*/
function handleArray(childSchema, source, target) {
const { items, additionalItems } = childSchema;
if (items !== undefined) {
if (Array.isArray(items)) {
for (let i = 0; i < items.length; i += 1) {
target.push(omit(items[i], source[i]));
}
}
else {
for (let i = 0; i < source.length; i += 1) {
target.push(omit(items, source[i]));
}
}
}
// additionalItems covers tuple items beyond the items array length.
if (additionalItems) {
for (let i = target.length; i < source.length; i += 1) {
target.push(omit(additionalItems, source[i]));
}
}
return target;
}
/** Applies the `if/then/else` conditional keywords from `schema` to `target`. When `schema.if` is
* absent the original `target` is returned unchanged. Otherwise the condition is evaluated — using
* `validator.isValid` for schema conditions or the boolean value directly — and the matching branch
* (`then` or `else`) is applied via `omit`. When the selected branch is absent, `target` is returned
* unchanged.
*
* @param childSchema - The schema potentially containing `if`, `then`, and `else` keywords
* @param source - The current form data value, passed to `validator.isValid` and the branch `omit`
* @param target - The already-filtered value to merge the branch result into
* @returns - The result of applying the matched branch, or `target` when no branch applies
*/
function handleConditions(childSchema, source, target) {
const { if: condition, then, else: otherwise } = childSchema;
if (condition === undefined) {
return target;
}
// validator.isValid signature: (schema, formData, rootSchema)
const isThenBranch = isSchemaObj(condition)
? validator.isValid(condition, source, rootSchema)
: condition;
const branch = isThenBranch ? then : otherwise;
return branch === undefined ? target : omit(branch, source, target);
}
/** Applies the best-matching `oneOf` option to `source`, merging the result into `target`. When
* `oneOf` is not an array or the schema represents a select widget (enum-driven), `target` is
* returned unchanged. `additionalProperties: false` is relaxed on each option before scoring so that
* `getClosestMatchingOption` can validate freely, but the original option schema is used for the
* actual `omit` call.
*
* @param oneOf - The `oneOf` array from the schema, or `undefined`
* @param childSchema - The parent schema containing the `oneOf` keyword
* @param source - The current form data value used to score each option
* @param target - The already-filtered value to merge the winning option's result into
* @returns - The result of applying the best-matching option, or `target` when no matching applies
*/
function handleOneOf(oneOf, childSchema, source, target) {
if (!Array.isArray(oneOf) || isSelect(validator, childSchema, rootSchema, experimental_customMergeAllOf)) {
return target;
}
// Resolve $refs and relax additionalProperties:false → true in one pass for scoring only.
// The unrelaxed resolved schema is re-derived for the winning option so that omit() still
// respects additionalProperties:false during filtering.
const scoringOptions = relaxOptionsForScoring(oneOf, true, rootSchema);
const bestIndex = getClosestMatchingOption(validator, rootSchema, source, scoringOptions, 0, getDiscriminatorFieldFromSchema(childSchema), experimental_customMergeAllOf);
const winning = oneOf[bestIndex];
// For object options, re-resolve without relaxation so additionalProperties:false is respected.
// For boolean options, scoringOptions already holds the converted schema (true→{}, false→{not:{}}).
const resolved = isObject(winning)
? resolveAllReferences(winning, rootSchema, [])
: scoringOptions[bestIndex];
return omit(resolved, source, target);
}
/** Applies `anyOf` branches from `schema` to `source`, merging results into `target`. When `anyOf`
* is absent or not an array, `target` is returned unchanged. For undefined or empty sources (so that
* defaults can flow through all branches) every branch is applied in sequence. For non-empty sources
* the best-matching branch is selected via `handleOneOf`.
*
* @param childSchema - The schema potentially containing an `anyOf` keyword
* @param source - The current form data value; empty or undefined triggers all-branch application
* @param target - The already-filtered value to merge branch results into
* @returns - The result after applying the relevant `anyOf` branch(es), or `target` when inapplicable
*/
function handleAnyOf(childSchema, source, target) {
const { anyOf } = childSchema;
if (!Array.isArray(anyOf)) {
return target;
}
// For undefined or empty collections, apply every branch so defaults flow through.
if (source === undefined ||
(Array.isArray(source) && source.length === 0) ||
(isObject(source) && Object.keys(source).length === 0)) {
let result = target;
for (const branch of anyOf) {
result = omit(branch, source, result);
}
return result;
}
return handleOneOf(anyOf, childSchema, source, target);
}
/** Applies schema-based `dependencies` from `schema` to `source`, merging each active dependency's
* schema into `target` via `omit`. Property dependencies (plain string arrays) are skipped — only
* schema dependencies are processed. A dependency is considered active when its trigger key is
* present on `source`.
*
* @param childSchema - The schema potentially containing a `dependencies` keyword
* @param source - The current form data value; must be a plain object for dependencies to apply
* @param target - The already-filtered value to merge dependency results into
* @returns - The result after applying all active schema dependencies, or `target` when inapplicable
*/
function handleDependencies(childSchema, source, target) {
const { dependencies } = childSchema;
if (dependencies === undefined || !isObjectValue(source)) {
return target;
}
let result = target;
for (const [key, deps] of Object.entries(dependencies)) {
// Skip property dependencies (string arrays); only process schema dependencies.
if (key in source && !Array.isArray(deps)) {
result = omit(deps, source, result);
}
}
return result;
}
/** Core recursive filter. Resolves `$ref`s, merges `allOf`, then delegates to the type-specific
* handlers (`handleObject`, `handleArray`) and keyword handlers (`handleAnyOf`, `handleOneOf`,
* `handleConditions`, `handleDependencies`). Returns `undefined` when `source` is undefined or
* `schemaDef` is `false`; returns `source` unchanged when `schemaDef` is `true` or empty.
*
* @param schemaDef - The schema (or boolean shorthand) to filter `source` against
* @param source - The raw form data value to filter
* @param [target] - An optional accumulator carrying results from prior oneOf/anyOf processing
* @returns - The filtered value, or `undefined` when the schema rejects the value
*/
function omit(schemaDef, source, target) {
if (source === undefined || schemaDef === false) {
return undefined;
}
if (schemaDef === true || isEmpty(schemaDef)) {
return source;
}
let localSchema = schemaDef;
const { $ref: ref, allOf } = localSchema;
if (ref !== undefined) {
return omit(findSchemaDefinition(ref, rootSchema), source, target);
}
if (allOf) {
localSchema = doMergeAllOf(localSchema, experimental_customMergeAllOf);
}
let filtered = handleAnyOf(localSchema, source, handleOneOf(localSchema.oneOf, localSchema, source, target));
const type = getSchemaType(localSchema);
if (type === 'object') {
if (!isObjectValue(source)) {
return undefined;
}
filtered = handleObject(localSchema, source, isObjectValue(filtered) ? filtered : {});
}
else if (type === 'array') {
if (!Array.isArray(source)) {
return undefined;
}
filtered = handleArray(localSchema, source, Array.isArray(filtered) ? filtered : []);
}
else if (filtered === undefined) {
filtered = source;
}
return handleDependencies(localSchema, source, handleConditions(localSchema, source, filtered));
}
return omit(schema, formData);
}
//# sourceMappingURL=omitExtraData.js.map