From fc0fef77de06978f0cbea3bf0eda61e9054862fa Mon Sep 17 00:00:00 2001 From: Adam Rimon Date: Tue, 24 Mar 2026 17:27:30 +0200 Subject: [PATCH 1/6] extended ui:emptyValue to apply on initial render and reset, not just widget clear Threaded uiSchema through the getDefaultFormState pipeline so that ui:emptyValue is used as a fallback whenever a field is blank, regardless of whether it became blank from initial state, reset, or user clearing. --- packages/core/src/components/Form.tsx | 1 + packages/utils/src/createSchemaUtils.ts | 3 + .../utils/src/schema/getDefaultFormState.ts | 28 +++- packages/utils/src/types.ts | 1 + .../test/schema/getDefaultFormStateTest.ts | 123 ++++++++++++++++++ 5 files changed, 152 insertions(+), 4 deletions(-) diff --git a/packages/core/src/components/Form.tsx b/packages/core/src/components/Form.tsx index be445e4054..6e93cf06c8 100644 --- a/packages/core/src/components/Form.tsx +++ b/packages/core/src/components/Form.tsx @@ -592,6 +592,7 @@ export default class Form< defaultsFormData, false, state.initialDefaultsGenerated, + uiSchema, ) as T; const _retrievedSchema = this.updateRetrievedSchema( retrievedSchema ?? schemaUtils.retrieveSchema(rootSchema, formData), diff --git a/packages/utils/src/createSchemaUtils.ts b/packages/utils/src/createSchemaUtils.ts index d95abc413a..8490678392 100644 --- a/packages/utils/src/createSchemaUtils.ts +++ b/packages/utils/src/createSchemaUtils.ts @@ -169,6 +169,7 @@ class SchemaUtils< * If "excludeObjectChildren", pass `includeUndefinedValues` as false when computing defaults for any nested * object properties. * @param initialDefaultsGenerated - Indicates whether or not initial defaults have been generated + * @param [uiSchema] - Optional uiSchema, used to apply ui:emptyValue and ui:initialValue as defaults * @returns - The resulting `formData` with all the defaults provided */ getDefaultFormState( @@ -176,6 +177,7 @@ class SchemaUtils< formData?: T, includeUndefinedValues: boolean | 'excludeObjectChildren' = false, initialDefaultsGenerated?: boolean, + uiSchema?: UiSchema, ): T | T[] | undefined { return getDefaultFormState( this.validator, @@ -186,6 +188,7 @@ class SchemaUtils< this.experimental_defaultFormStateBehavior, this.experimental_customMergeAllOf, initialDefaultsGenerated, + uiSchema, ); } diff --git a/packages/utils/src/schema/getDefaultFormState.ts b/packages/utils/src/schema/getDefaultFormState.ts index 815e734bec..79685afa49 100644 --- a/packages/utils/src/schema/getDefaultFormState.ts +++ b/packages/utils/src/schema/getDefaultFormState.ts @@ -29,8 +29,10 @@ import { GenericObjectType, RJSFSchema, StrictRJSFSchema, + UiSchema, ValidatorType, } from '../types'; +import getUiOptions from '../getUiOptions'; import isMultiSelect from './isMultiSelect'; import isSelect from './isSelect'; import retrieveSchema, { resolveDependencies } from './retrieveSchema'; @@ -183,7 +185,7 @@ function maybeAddDefaultToObject( } } -interface ComputeDefaultsProps { +interface ComputeDefaultsProps { /** Any defaults provided by the parent field in the schema */ parentDefaults?: T; /** The options root schema, used to primarily to look up `$ref`s */ @@ -211,6 +213,8 @@ interface ComputeDefaultsProps shouldMergeDefaultsIntoFormData?: boolean; /** Indicates whether initial defaults have been generated */ initialDefaultsGenerated?: boolean; + /** Optional uiSchema, used to apply ui:emptyValue and ui:initialValue as defaults */ + uiSchema?: UiSchema; } /** Computes the defaults for the current `schema` given the `rawFormData` and `parentDefaults` if any. This drills into @@ -224,7 +228,7 @@ interface ComputeDefaultsProps export function computeDefaults( validator: ValidatorType, rawSchema: S, - computeDefaultsProps: ComputeDefaultsProps = {}, + computeDefaultsProps: ComputeDefaultsProps = {}, ): T | T[] | undefined { const { parentDefaults, @@ -237,6 +241,7 @@ export function computeDefaults - computeDefaults(validator, itemSchema, { + computeDefaults(validator, itemSchema, { rootSchema, includeUndefinedValues, _recurseList, @@ -381,6 +386,7 @@ export function computeDefaults = {}, + uiSchema, + }: ComputeDefaultsProps = {}, defaults?: T | T[], ): T { { @@ -522,6 +537,7 @@ export function getObjectDefaults( @@ -571,6 +587,7 @@ export function getObjectDefaults( @@ -761,6 +778,7 @@ export function getDefaultBasedOnSchemaType< * @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 * @param initialDefaultsGenerated - Optional flag, indicates whether or not initial defaults have been generated + * @param [uiSchema] - Optional uiSchema, used to apply ui:emptyValue and ui:initialValue as defaults * @returns - The resulting `formData` with all the defaults provided */ export default function getDefaultFormState< @@ -776,6 +794,7 @@ export default function getDefaultFormState< experimental_defaultFormStateBehavior?: Experimental_DefaultFormStateBehavior, experimental_customMergeAllOf?: Experimental_CustomMergeAllOf, initialDefaultsGenerated?: boolean, + uiSchema?: UiSchema, ) { if (!isObject(theSchema)) { throw new Error('Invalid schema: ' + theSchema); @@ -794,6 +813,7 @@ export default function getDefaultFormState< shouldMergeDefaultsIntoFormData: true, initialDefaultsGenerated, requiredAsRoot: true, + uiSchema, }); if (schema.type !== 'object' && isObject(schema.default)) { diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index d92f87806f..860e401354 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -1329,6 +1329,7 @@ export interface SchemaUtilsType, ): T | T[] | undefined; /** Determines whether the combination of `schema` and `uiSchema` properties indicates that the label for the `schema` * should be displayed in a UI. diff --git a/packages/utils/test/schema/getDefaultFormStateTest.ts b/packages/utils/test/schema/getDefaultFormStateTest.ts index f493383be8..b0f22ec870 100644 --- a/packages/utils/test/schema/getDefaultFormStateTest.ts +++ b/packages/utils/test/schema/getDefaultFormStateTest.ts @@ -5691,5 +5691,128 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType expect((result![0] as any).nested).not.toBe((result![1] as any).nested); }); }); + + describe('ui:emptyValue in getDefaultFormState', () => { + it('uses emptyValue as default when no schema default and no formData', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + name: { type: 'string' }, + }, + }; + const uiSchema = { + name: { 'ui:emptyValue': '' }, + }; + expect( + getDefaultFormState( + testValidator, + schema, + undefined, + schema, + false, + undefined, + undefined, + undefined, + uiSchema, + ), + ).toEqual({ + name: '', + }); + }); + it('does not use emptyValue when schema default exists', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + name: { type: 'string', default: 'hello' }, + }, + }; + const uiSchema = { + name: { 'ui:emptyValue': '' }, + }; + expect( + getDefaultFormState( + testValidator, + schema, + undefined, + schema, + false, + undefined, + undefined, + undefined, + uiSchema, + ), + ).toEqual({ + name: 'hello', + }); + }); + it('does not use emptyValue when formData is provided', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + name: { type: 'string' }, + }, + }; + const uiSchema = { + name: { 'ui:emptyValue': '' }, + }; + expect( + getDefaultFormState( + testValidator, + schema, + { name: 'world' }, + schema, + false, + undefined, + undefined, + undefined, + uiSchema, + ), + ).toEqual({ + name: 'world', + }); + }); + it('applies emptyValue in nested objects', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + address: { + type: 'object', + properties: { + city: { type: 'string' }, + }, + }, + }, + }; + const uiSchema = { + address: { + city: { 'ui:emptyValue': '' }, + }, + }; + expect( + getDefaultFormState( + testValidator, + schema, + undefined, + schema, + false, + undefined, + undefined, + undefined, + uiSchema, + ), + ).toEqual({ + address: { city: '' }, + }); + }); + it('behaves unchanged when uiSchema is not passed', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + name: { type: 'string' }, + }, + }; + expect(getDefaultFormState(testValidator, schema, undefined, schema)).toEqual({}); + }); + }); }); } From 42cbb2e92b3ef54a39dafa004b441881ea62e9e7 Mon Sep 17 00:00:00 2001 From: Adam Rimon Date: Tue, 24 Mar 2026 17:28:17 +0200 Subject: [PATCH 2/6] added ui:initialValue support to pre-fill fields on render and reset ui:initialValue takes priority over schema.default and is used when the form is first rendered or after a reset. It does not override user-provided formData. emptyValue remains the fallback for blank fields. --- .../utils/src/schema/getDefaultFormState.ts | 8 +- packages/utils/src/types.ts | 2 + .../test/schema/getDefaultFormStateTest.ts | 167 ++++++++++++++++++ 3 files changed, 174 insertions(+), 3 deletions(-) diff --git a/packages/utils/src/schema/getDefaultFormState.ts b/packages/utils/src/schema/getDefaultFormState.ts index 79685afa49..98f30864c9 100644 --- a/packages/utils/src/schema/getDefaultFormState.ts +++ b/packages/utils/src/schema/getDefaultFormState.ts @@ -395,10 +395,12 @@ export function computeDefaults { + it('uses initialValue as default when no formData and no schema default', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + country: { type: 'string' }, + }, + }; + const uiSchema = { + country: { 'ui:initialValue': 'US' }, + }; + expect( + getDefaultFormState( + testValidator, + schema, + undefined, + schema, + false, + undefined, + undefined, + undefined, + uiSchema, + ), + ).toEqual({ + country: 'US', + }); + }); + it('overrides schema default with initialValue', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + country: { type: 'string', default: 'UK' }, + }, + }; + const uiSchema = { + country: { 'ui:initialValue': 'US' }, + }; + expect( + getDefaultFormState( + testValidator, + schema, + undefined, + schema, + false, + undefined, + undefined, + undefined, + uiSchema, + ), + ).toEqual({ + country: 'US', + }); + }); + it('does not override provided formData', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + country: { type: 'string' }, + }, + }; + const uiSchema = { + country: { 'ui:initialValue': 'US' }, + }; + expect( + getDefaultFormState( + testValidator, + schema, + { country: 'FR' }, + schema, + false, + undefined, + undefined, + undefined, + uiSchema, + ), + ).toEqual({ + country: 'FR', + }); + }); + it('takes priority over emptyValue', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + country: { type: 'string' }, + }, + }; + const uiSchema = { + country: { 'ui:initialValue': 'US', 'ui:emptyValue': '' }, + }; + expect( + getDefaultFormState( + testValidator, + schema, + undefined, + schema, + false, + undefined, + undefined, + undefined, + uiSchema, + ), + ).toEqual({ + country: 'US', + }); + }); + it('applies initialValue in nested objects', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + address: { + type: 'object', + properties: { + country: { type: 'string' }, + city: { type: 'string' }, + }, + }, + }, + }; + const uiSchema = { + address: { + country: { 'ui:initialValue': 'US' }, + }, + }; + expect( + getDefaultFormState( + testValidator, + schema, + undefined, + schema, + false, + undefined, + undefined, + undefined, + uiSchema, + ), + ).toEqual({ + address: { country: 'US' }, + }); + }); + it('applies initialValue on reset (undefined formData)', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + name: { type: 'string' }, + }, + }; + const uiSchema = { + name: { 'ui:initialValue': 'default name' }, + }; + expect( + getDefaultFormState( + testValidator, + schema, + undefined, + schema, + false, + undefined, + undefined, + undefined, + uiSchema, + ), + ).toEqual({ + name: 'default name', + }); + }); + }); }); } From 88754a5a00bcb1619262b784e0721f584f4b55e5 Mon Sep 17 00:00:00 2001 From: Adam Rimon Date: Tue, 24 Mar 2026 17:33:07 +0200 Subject: [PATCH 3/6] added ui:required support for overriding required status from uiSchema ui:required: true adds the field to validation and shows the required indicator. ui:required: false hides the indicator but schema validation still runs. Emits console.warn when ui:required: false is used on a schema-required field without ui:initialValue or ui:emptyValue. --- packages/core/src/components/Form.tsx | 4 +- .../src/components/fields/LayoutGridField.tsx | 4 +- .../src/components/fields/SchemaField.tsx | 16 ++- packages/core/test/uiSchema.test.tsx | 96 ++++++++++++++++ .../utils/src/augmentSchemaWithUiRequired.ts | 59 ++++++++++ packages/utils/src/index.ts | 2 + packages/utils/src/types.ts | 2 + .../test/augmentSchemaWithUiRequired.test.ts | 107 ++++++++++++++++++ 8 files changed, 286 insertions(+), 4 deletions(-) create mode 100644 packages/utils/src/augmentSchemaWithUiRequired.ts create mode 100644 packages/utils/test/augmentSchemaWithUiRequired.test.ts diff --git a/packages/core/src/components/Form.tsx b/packages/core/src/components/Form.tsx index 6e93cf06c8..1cd2ce2f8c 100644 --- a/packages/core/src/components/Form.tsx +++ b/packages/core/src/components/Form.tsx @@ -1,5 +1,6 @@ import { Component, ElementType, FormEvent, ReactNode, Ref, RefObject, createRef } from 'react'; import { + augmentSchemaWithUiRequired, createSchemaUtils, CustomValidator, deepEquals, @@ -720,9 +721,10 @@ export default class Form< const schemaUtils = altSchemaUtils ? altSchemaUtils : this.state.schemaUtils; const { customValidate, transformErrors, uiSchema } = this.props; const resolvedSchema = retrievedSchema ?? schemaUtils.retrieveSchema(schema, formData); + const effectiveSchema = augmentSchemaWithUiRequired(resolvedSchema, uiSchema); return schemaUtils .getValidator() - .validateFormData(formData, resolvedSchema, customValidate, transformErrors, uiSchema); + .validateFormData(formData, effectiveSchema, customValidate, transformErrors, uiSchema); } /** Renders any errors contained in the `state` in using the `ErrorList`, if not disabled by `showErrorList`. */ diff --git a/packages/core/src/components/fields/LayoutGridField.tsx b/packages/core/src/components/fields/LayoutGridField.tsx index bc5374b518..cb42154b58 100644 --- a/packages/core/src/components/fields/LayoutGridField.tsx +++ b/packages/core/src/components/fields/LayoutGridField.tsx @@ -681,6 +681,8 @@ function LayoutGridFieldComponent(name, uiProps, uiSchema, isReadonly, readonly); + const fieldUiOptions = getUiOptions(fieldUiSchema); + const effectiveRequired = fieldUiOptions.required !== undefined ? Boolean(fieldUiOptions.required) : isRequired; return ( (schema, uiOptions, registry); const disabled = Boolean(uiOptions.disabled ?? props.disabled); const readonly = Boolean(uiOptions.readonly ?? (props.readonly || props.schema.readOnly || schema.readOnly)); + const effectiveRequired = uiOptions.required !== undefined ? Boolean(uiOptions.required) : required; + if ( + uiOptions.required === false && + required && + uiOptions.initialValue === undefined && + uiOptions.emptyValue === undefined + ) { + console.warn( + `ui:required is false for a schema-required field "${name}" but no ui:initialValue or ui:emptyValue is set. ` + + 'The UI will show this field as optional but schema validation will still fail if left empty.', + ); + } const uiSchemaHideError = uiOptions.hideError; // Set hideError to the value provided in the uiSchema, otherwise stick with the prop to propagate to children const hideError = uiSchemaHideError === undefined ? props.hideError : Boolean(uiSchemaHideError); @@ -167,7 +179,7 @@ function SchemaFieldRender(registry, schema, required, uiSchema); + const isOptionalRender = shouldRenderOptionalField(registry, schema, effectiveRequired, uiSchema); const hasFormData = isFormDataAvailable(formData); displayLabel = displayLabel && (!isOptionalRender || hasFormData); fieldPathIdProps = { @@ -274,7 +286,7 @@ function SchemaFieldRender { expect(node.querySelectorAll("input[placeholder='Node name']")).toHaveLength(5); }); }); + + describe('ui:required', () => { + it('shows required asterisk on non-required field when ui:required is true', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + foo: { type: 'string' }, + }, + }; + const uiSchema: UiSchema = { + foo: { 'ui:required': true }, + }; + const { node } = createFormComponent({ schema, uiSchema }); + expect(node.querySelector('.rjsf-field-string label span.required')).not.toBeNull(); + }); + + it('hides required asterisk on schema-required field when ui:required is false', () => { + const schema: RJSFSchema = { + type: 'object', + required: ['foo'], + properties: { + foo: { type: 'string' }, + }, + }; + const uiSchema: UiSchema = { + foo: { 'ui:required': false, 'ui:initialValue': 'fallback' }, + }; + const { node } = createFormComponent({ schema, uiSchema }); + expect(node.querySelector('.rjsf-field-string label span.required')).toBeNull(); + }); + + it('produces validation error when ui:required is true and field is empty', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + foo: { type: 'string' }, + }, + }; + const uiSchema: UiSchema = { + foo: { 'ui:required': true }, + }; + const { node, onSubmit } = createFormComponent({ schema, uiSchema }); + submitForm(node); + expect(onSubmit).not.toHaveBeenCalled(); + }); + + it('does not suppress schema validation when ui:required is false', () => { + const schema: RJSFSchema = { + type: 'object', + required: ['foo'], + properties: { + foo: { type: 'string' }, + }, + }; + const uiSchema: UiSchema = { + foo: { 'ui:required': false }, + }; + const { node, onSubmit } = createFormComponent({ schema, uiSchema }); + submitForm(node); + expect(onSubmit).not.toHaveBeenCalled(); + }); + + it('emits console.warn for ui:required false without initialValue or emptyValue', () => { + const schema: RJSFSchema = { + type: 'object', + required: ['foo'], + properties: { + foo: { type: 'string' }, + }, + }; + const uiSchema: UiSchema = { + foo: { 'ui:required': false }, + }; + createFormComponent({ schema, uiSchema }); + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining('ui:required is false for a schema-required field'), + ); + }); + + it('does not emit console.warn for ui:required false with initialValue set', () => { + const schema: RJSFSchema = { + type: 'object', + required: ['foo'], + properties: { + foo: { type: 'string' }, + }, + }; + const uiSchema: UiSchema = { + foo: { 'ui:required': false, 'ui:initialValue': 'x' }, + }; + createFormComponent({ schema, uiSchema }); + expect(consoleWarnSpy).not.toHaveBeenCalledWith( + expect.stringContaining('ui:required is false for a schema-required field'), + ); + }); + }); }); diff --git a/packages/utils/src/augmentSchemaWithUiRequired.ts b/packages/utils/src/augmentSchemaWithUiRequired.ts new file mode 100644 index 0000000000..0369c2d59b --- /dev/null +++ b/packages/utils/src/augmentSchemaWithUiRequired.ts @@ -0,0 +1,59 @@ +import get from 'lodash/get'; + +import getUiOptions from './getUiOptions'; +import { FormContextType, RJSFSchema, StrictRJSFSchema, UiSchema } from './types'; + +/** Recursively walks a schema and uiSchema, adding fields marked `ui:required: true` to the schema's `required` + * arrays. Returns a new schema without mutating the original. + * + * @param schema - The schema to augment + * @param [uiSchema] - The uiSchema containing ui:required overrides + * @returns A new schema with ui:required fields added to required arrays + */ +export default function augmentSchemaWithUiRequired< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(schema: S, uiSchema?: UiSchema): S { + if (!uiSchema || schema.type !== 'object' || !schema.properties) { + return schema; + } + + const existingRequired = schema.required || []; + const additionalRequired: string[] = []; + let propertiesChanged = false; + const newProperties: Record = {}; + + for (const key of Object.keys(schema.properties)) { + const fieldUiSchema = get(uiSchema, [key]); + const propertySchema = schema.properties[key] as S; + + if (fieldUiSchema) { + const { required: uiRequired } = getUiOptions(fieldUiSchema); + if (uiRequired === true && !existingRequired.includes(key)) { + additionalRequired.push(key); + } + } + + // Recurse into nested objects + if (propertySchema && propertySchema.type === 'object' && fieldUiSchema) { + const augmented = augmentSchemaWithUiRequired(propertySchema, fieldUiSchema); + if (augmented !== propertySchema) { + propertiesChanged = true; + newProperties[key] = augmented; + continue; + } + } + newProperties[key] = propertySchema; + } + + if (additionalRequired.length === 0 && !propertiesChanged) { + return schema; + } + + return { + ...schema, + ...(propertiesChanged ? { properties: newProperties } : {}), + ...(additionalRequired.length > 0 ? { required: [...existingRequired, ...additionalRequired] } : {}), + }; +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 1521440899..d85b3bc183 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,5 +1,6 @@ import allowAdditionalItems from './allowAdditionalItems'; import asNumber from './asNumber'; +import augmentSchemaWithUiRequired from './augmentSchemaWithUiRequired'; import canExpand from './canExpand'; import createErrorHandler from './createErrorHandler'; import createSchemaUtils from './createSchemaUtils'; @@ -97,6 +98,7 @@ export { allowAdditionalItems, ariaDescribedByIds, asNumber, + augmentSchemaWithUiRequired, buttonId, canExpand, createErrorHandler, diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index 57f79e11da..4ac5b0e0f9 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -1075,6 +1075,8 @@ type UIOptionsBaseType { + it('returns the same schema when no uiSchema is provided', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { foo: { type: 'string' } }, + }; + expect(augmentSchemaWithUiRequired(schema)).toBe(schema); + }); + + it('returns the same schema when no ui:required fields exist', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { foo: { type: 'string' } }, + }; + const uiSchema: UiSchema = { foo: { 'ui:widget': 'textarea' } }; + expect(augmentSchemaWithUiRequired(schema, uiSchema)).toBe(schema); + }); + + it('adds ui:required: true field to required array', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { foo: { type: 'string' }, bar: { type: 'string' } }, + }; + const uiSchema: UiSchema = { foo: { 'ui:required': true } }; + const result = augmentSchemaWithUiRequired(schema, uiSchema); + expect(result.required).toEqual(['foo']); + expect(result).not.toBe(schema); + }); + + it('does not duplicate existing required fields', () => { + const schema: RJSFSchema = { + type: 'object', + required: ['foo'], + properties: { foo: { type: 'string' } }, + }; + const uiSchema: UiSchema = { foo: { 'ui:required': true } }; + const result = augmentSchemaWithUiRequired(schema, uiSchema); + expect(result).toBe(schema); + }); + + it('preserves existing required and adds new ones', () => { + const schema: RJSFSchema = { + type: 'object', + required: ['foo'], + properties: { foo: { type: 'string' }, bar: { type: 'string' } }, + }; + const uiSchema: UiSchema = { bar: { 'ui:required': true } }; + const result = augmentSchemaWithUiRequired(schema, uiSchema); + expect(result.required).toEqual(['foo', 'bar']); + }); + + it('handles nested objects', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + address: { + type: 'object', + properties: { + city: { type: 'string' }, + }, + }, + }, + }; + const uiSchema: UiSchema = { + address: { city: { 'ui:required': true } }, + }; + const result = augmentSchemaWithUiRequired(schema, uiSchema); + expect((result.properties!.address as RJSFSchema).required).toEqual(['city']); + }); + + it('does not mutate the original schema', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { foo: { type: 'string' } }, + }; + const uiSchema: UiSchema = { foo: { 'ui:required': true } }; + augmentSchemaWithUiRequired(schema, uiSchema); + expect(schema.required).toBeUndefined(); + }); + + it('returns same schema when nested object has uiSchema but no ui:required changes', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + address: { + type: 'object', + properties: { + city: { type: 'string' }, + }, + }, + }, + }; + const uiSchema: UiSchema = { + address: { city: { 'ui:widget': 'textarea' } }, + }; + const result = augmentSchemaWithUiRequired(schema, uiSchema); + expect(result).toBe(schema); + }); + + it('returns schema as-is for non-object types', () => { + const schema: RJSFSchema = { type: 'string' }; + const uiSchema: UiSchema = { 'ui:required': true }; + expect(augmentSchemaWithUiRequired(schema, uiSchema)).toBe(schema); + }); +}); From d3009c198a43e6eaf6513942445fb56fafb8bd97 Mon Sep 17 00:00:00 2001 From: Adam Rimon Date: Wed, 25 Mar 2026 14:55:15 +0200 Subject: [PATCH 4/6] updated playground --- packages/playground/src/samples/options.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/playground/src/samples/options.ts b/packages/playground/src/samples/options.ts index 566636c00a..035ffeef82 100644 --- a/packages/playground/src/samples/options.ts +++ b/packages/playground/src/samples/options.ts @@ -21,6 +21,14 @@ const optionsSample: Sample = { title: 'Telephone', minLength: 10, }, + country: { + type: 'string', + title: 'Country', + }, + email: { + type: 'string', + title: 'Email', + }, }, }, uiSchema: { @@ -41,6 +49,7 @@ const optionsSample: Sample = { 'ui:title': 'Surname', 'ui:emptyValue': '', 'ui:autocomplete': 'given-name', + 'ui:required': false, }, age: { 'ui:widget': 'updown', @@ -62,6 +71,13 @@ const optionsSample: Sample = { inputType: 'tel', }, }, + country: { + 'ui:initialValue': 'US', + 'ui:widget': 'hidden', + }, + email: { + 'ui:required': true, + }, }, formData: { lastName: 'Norris', From 916f66c9fab5d3aa166c5a50e47070abdd9d8557 Mon Sep 17 00:00:00 2001 From: Adam Rimon Date: Wed, 25 Mar 2026 14:58:49 +0200 Subject: [PATCH 5/6] updated docs --- packages/docs/docs/api-reference/uiSchema.md | 46 +++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/docs/docs/api-reference/uiSchema.md b/packages/docs/docs/api-reference/uiSchema.md index 1e58a6f947..6cef61937f 100644 --- a/packages/docs/docs/api-reference/uiSchema.md +++ b/packages/docs/docs/api-reference/uiSchema.md @@ -485,7 +485,25 @@ render( ### emptyValue -The `ui:emptyValue` uiSchema directive provides the default value to use when an input for a field is empty +The `ui:emptyValue` uiSchema directive provides the default value to use when a field is empty. This applies whenever the field is blank, whether from initial render, a form reset, or the user clearing the input. + +```tsx +import { RJSFSchema, UiSchema } from '@rjsf/utils'; + +const schema: RJSFSchema = { + type: 'object', + required: ['name'], + properties: { + name: { type: 'string' }, + }, +}; + +const uiSchema: UiSchema = { + name: { + 'ui:emptyValue': '', + }, +}; +``` ### enumDisabled @@ -588,6 +606,28 @@ If you need to enable the default error display of a child in the hierarchy afte This is useful when you have a custom field or widget that utilizes either the `rawErrors` or the `errorSchema` to manipulate and/or show the error(s) for the field/widget itself. +### initialValue + +The `ui:initialValue` uiSchema directive pre-fills a field on initial render and after a form reset. It takes priority over `schema.default` but does not override user-provided `formData`. Useful for hidden fields that need a fixed value. + +```tsx +import { RJSFSchema, UiSchema } from '@rjsf/utils'; + +const schema: RJSFSchema = { + type: 'object', + properties: { + country: { type: 'string' }, + }, +}; + +const uiSchema: UiSchema = { + country: { + 'ui:initialValue': 'US', + 'ui:widget': 'hidden', + }, +}; +``` + ### inputType To change the input type (for example, `tel` or `email`) you can specify the `inputType` in the `ui:options` uiSchema directive. @@ -682,6 +722,10 @@ The `ui:readonly` uiSchema directive will mark all child widgets from a given fi > Note: If you're wondering about the difference between a `disabled` field and a `readonly` one: Marking a field as read-only will render it greyed out, but its text value will be selectable. Disabling it will prevent its value to be selected at all. +### required + +The `ui:required` uiSchema directive overrides the schema's `required` status for a field. Setting `ui:required` to `true` shows the required indicator and produces a validation error if the field is left empty. Setting it to `false` hides the required indicator, but does not suppress schema-level validation. A `console.warn` is emitted when `ui:required` is `false` on a schema-required field without `ui:initialValue` or `ui:emptyValue` set. + ### rows You can set the initial height of a textarea widget by specifying `rows` option. From d2bc4cfb6f7701e8f8c8006cefb90a58769ea3e5 Mon Sep 17 00:00:00 2001 From: Adam Rimon Date: Wed, 25 Mar 2026 14:59:06 +0200 Subject: [PATCH 6/6] updated changelog --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 390e38ea01..6194142a7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,23 @@ should change the heading of the (upcoming) version to include a major version b --> +# 7.0.0 + +## @rjsf/utils + +- Extended `ui:emptyValue` to apply whenever a field is blank (initial render, reset, or user clearing), not just on widget clear ([#4983](https://github.com/rjsf-team/react-jsonschema-form/issues/4983)) +- Added `ui:initialValue` to pre-fill fields on render/reset, taking priority over `schema.default` ([#4983](https://github.com/rjsf-team/react-jsonschema-form/issues/4983)) +- Added `ui:required` to override schema required status from uiSchema ([#4983](https://github.com/rjsf-team/react-jsonschema-form/issues/4983)) + +## @rjsf/core + +- Updated `Form` to pass `uiSchema` through `getDefaultFormState` for `ui:emptyValue` and `ui:initialValue` support ([#4983](https://github.com/rjsf-team/react-jsonschema-form/issues/4983)) +- Updated `SchemaField` and `LayoutGridField` to respect `ui:required` override ([#4983](https://github.com/rjsf-team/react-jsonschema-form/issues/4983)) + +## Dev / docs / playground + +- Added documentation and playground examples for `ui:emptyValue` (extended), `ui:initialValue`, and `ui:required` ([#4983](https://github.com/rjsf-team/react-jsonschema-form/issues/4983)) + # 6.4.2 ## @rjsf/core