Skip to content

Commit ceb7fe3

Browse files
authored
feat: add ui:definitions for recursive and reusable uiSchema (#4947)
* Added ui:definitions for recursive uiSchema support * Added documentation * added CHANGELOG entries for ui:definitions * removed ui:definitions embedding, use registry instead * added empty uiSchemaDefinitions if not present * refactored expandUiSchemaDefinitions to use registry * fixed registry tests
1 parent 7395afc commit ceb7fe3

12 files changed

Lines changed: 841 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ should change the heading of the (upcoming) version to include a major version b
3030
## @rjsf/core
3131

3232
- Fixed duplicate React keys in datalist when schema examples and default have different types, fixing [#4927](https://github.com/rjsf-team/react-jsonschema-form/issues/4927)
33+
- Integrated `ui:definitions` support for recursive and reusable uiSchema ([#4947](https://github.com/rjsf-team/react-jsonschema-form/pull/4947))
3334

3435
## @rjsf/daisyui
3536

@@ -58,6 +59,16 @@ should change the heading of the (upcoming) version to include a major version b
5859
## @rjsf/shadcn
5960

6061
- Fixed duplicate React keys in datalist when schema examples and default have different types, fixing [#4927](https://github.com/rjsf-team/react-jsonschema-form/issues/4927)
62+
63+
## @rjsf/utils
64+
65+
- Added `expandUiSchemaDefinitions()` and `resolveUiSchema()` functions, and `UiSchemaDefinitions` type to support defining reusable uiSchema for schema `$ref` references ([#4947](https://github.com/rjsf-team/react-jsonschema-form/pull/4947))
66+
67+
## Dev / docs / playground
68+
69+
- Updated References sample in playground to demonstrate `ui:definitions` feature ([#4947](https://github.com/rjsf-team/react-jsonschema-form/pull/4947))
70+
- Added documentation for `ui:definitions` in `uiSchema.md` and `definitions.md` ([#4947](https://github.com/rjsf-team/react-jsonschema-form/pull/4947))
71+
6172
# 6.2.5
6273

6374
## @rjsf/mui

packages/core/src/components/Form.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
ErrorSchema,
77
ErrorSchemaBuilder,
88
ErrorTransformer,
9+
expandUiSchemaDefinitions,
910
FieldPathId,
1011
FieldPathList,
1112
FormContextType,
@@ -28,6 +29,7 @@ import {
2829
toErrorList,
2930
toFieldPathId,
3031
UiSchema,
32+
UI_DEFINITIONS_KEY,
3133
UI_GLOBAL_OPTIONS_KEY,
3234
UI_OPTIONS_KEY,
3335
ValidationData,
@@ -620,6 +622,12 @@ export default class Form<
620622
// Only store a new registry when the props cause a different one to be created
621623
const newRegistry = this.getRegistry(props, rootSchema, schemaUtils);
622624
const registry = deepEquals(state.registry, newRegistry) ? state.registry : newRegistry;
625+
626+
// Pre-expand ui:definitions into the uiSchema structure (must happen after registry is created)
627+
const expandedUiSchema: UiSchema<T, S, F> = registry.uiSchemaDefinitions
628+
? expandUiSchemaDefinitions<T, S, F>(rootSchema, uiSchema, registry)
629+
: uiSchema;
630+
623631
// Only compute a new `fieldPathId` when the `idPrefix` is different than the existing fieldPathId's ID_KEY
624632
const fieldPathId =
625633
state.fieldPathId && state.fieldPathId?.[ID_KEY] === registry.globalFormOptions.idPrefix
@@ -628,7 +636,7 @@ export default class Form<
628636
const nextState: FormState<T, S, F> = {
629637
schemaUtils,
630638
schema: rootSchema,
631-
uiSchema,
639+
uiSchema: expandedUiSchema,
632640
fieldPathId,
633641
formData,
634642
edit,
@@ -1149,6 +1157,7 @@ export default class Form<
11491157
translateString: customTranslateString || translateString,
11501158
globalUiOptions: uiSchema[UI_GLOBAL_OPTIONS_KEY],
11511159
globalFormOptions: this.getGlobalFormOptions(props),
1160+
uiSchemaDefinitions: uiSchema[UI_DEFINITIONS_KEY] ?? {},
11521161
};
11531162
}
11541163

packages/core/src/components/fields/SchemaField.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
isFormDataAvailable,
1818
ONE_OF_KEY,
1919
Registry,
20+
resolveUiSchema,
2021
RJSFSchema,
2122
shouldRender,
2223
shouldRenderOptionalField,
@@ -93,7 +94,7 @@ function SchemaFieldRender<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
9394
const {
9495
schema: _schema,
9596
fieldPathId,
96-
uiSchema,
97+
uiSchema: _uiSchema,
9798
formData,
9899
errorSchema,
99100
name,
@@ -107,6 +108,7 @@ function SchemaFieldRender<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
107108
} = props;
108109
const { schemaUtils, globalFormOptions, globalUiOptions, fields } = registry;
109110
const { AnyOfField: _AnyOfField, OneOfField: _OneOfField } = fields;
111+
const uiSchema = resolveUiSchema<T, S, F>(_schema, _uiSchema, registry);
110112
const uiOptions = getUiOptions<T, S, F>(uiSchema, globalUiOptions);
111113
const FieldTemplate = getTemplate<'FieldTemplate', T, S, F>('FieldTemplate', registry, uiOptions);
112114
const DescriptionFieldTemplate = getTemplate<'DescriptionFieldTemplate', T, S, F>(

packages/core/test/SchemaField.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ describe('SchemaField', () => {
5858
idSeparator: DEFAULT_ID_SEPARATOR,
5959
useFallbackUiForUnsupportedType: false,
6060
},
61+
uiSchemaDefinitions: {},
6162
});
6263
});
6364
it('should provide expected registry with globalUiOptions as prop', () => {
@@ -98,6 +99,7 @@ describe('SchemaField', () => {
9899
idSeparator: DEFAULT_ID_SEPARATOR,
99100
useFallbackUiForUnsupportedType: false,
100101
},
102+
uiSchemaDefinitions: {},
101103
});
102104
});
103105
});

packages/docs/docs/api-reference/uiSchema.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,106 @@ const uiSchema: UiSchema = {
6363
};
6464
```
6565

66+
### ui:definitions
67+
68+
The `ui:definitions` property allows you to define reusable UI customizations for schema `$ref` references. This is particularly useful for:
69+
70+
- **Recursive schemas** - Define uiSchema once for a self-referencing schema node
71+
- **Reused definitions** - Apply consistent UI to schemas used in multiple places
72+
- **oneOf/anyOf branches** - Provide different uiSchema for branches that reference different definitions
73+
74+
The keys in `ui:definitions` must match the exact `$ref` path used in the schema (e.g., `#/definitions/address` or `#/$defs/node`).
75+
76+
```tsx
77+
import { Form } from '@rjsf/core';
78+
import { RJSFSchema, UiSchema } from '@rjsf/utils';
79+
import validator from '@rjsf/validator-ajv8';
80+
81+
const schema: RJSFSchema = {
82+
definitions: {
83+
address: {
84+
type: 'object',
85+
properties: {
86+
street_address: { type: 'string' },
87+
city: { type: 'string' },
88+
},
89+
},
90+
node: {
91+
type: 'object',
92+
properties: {
93+
name: { type: 'string' },
94+
children: {
95+
type: 'array',
96+
items: { $ref: '#/definitions/node' }, // Recursive reference
97+
},
98+
},
99+
},
100+
},
101+
type: 'object',
102+
properties: {
103+
billing_address: { $ref: '#/definitions/address' },
104+
shipping_address: { $ref: '#/definitions/address' },
105+
tree: { $ref: '#/definitions/node' },
106+
},
107+
};
108+
109+
const uiSchema: UiSchema = {
110+
'ui:definitions': {
111+
'#/definitions/address': {
112+
street_address: { 'ui:placeholder': 'Street and number' },
113+
city: { 'ui:placeholder': 'City name' },
114+
},
115+
'#/definitions/node': {
116+
name: { 'ui:placeholder': 'Enter node name' },
117+
children: { 'ui:options': { orderable: false } },
118+
},
119+
},
120+
// Local overrides take precedence over ui:definitions
121+
shipping_address: {
122+
street_address: { 'ui:placeholder': 'Shipping street (overrides definition)' },
123+
},
124+
};
125+
126+
render(<Form schema={schema} uiSchema={uiSchema} validator={validator} />, document.getElementById('app'));
127+
```
128+
129+
#### Local Overrides
130+
131+
You can override specific properties from `ui:definitions` by providing values at the field path. Local values are merged with definitions, with local values taking precedence.
132+
133+
#### oneOf/anyOf with Same Property Names
134+
135+
When using `oneOf` or `anyOf` with branches that have properties with the same name, each branch's `$ref` maps to its own entry in `ui:definitions`, ensuring correct UI is applied:
136+
137+
```tsx
138+
const schema: RJSFSchema = {
139+
definitions: {
140+
person: { type: 'object', properties: { name: { type: 'string' } } },
141+
company: { type: 'object', properties: { name: { type: 'string' } } },
142+
},
143+
type: 'object',
144+
properties: {
145+
contact: {
146+
oneOf: [
147+
{ title: 'Person', $ref: '#/definitions/person' },
148+
{ title: 'Company', $ref: '#/definitions/company' },
149+
],
150+
},
151+
},
152+
};
153+
154+
const uiSchema: UiSchema = {
155+
'ui:definitions': {
156+
'#/definitions/person': {
157+
name: { 'ui:placeholder': 'Full name (e.g., John Doe)' },
158+
},
159+
'#/definitions/company': {
160+
name: { 'ui:placeholder': 'Company name (e.g., Acme Inc.)' },
161+
},
162+
},
163+
};
164+
```
165+
66166
### ui:rootFieldId (deprecated)
67167

68168
> DEPRECATED: Use `Form.idPrefix` instead, will be removed in a future major version

packages/docs/docs/json-schema/definitions.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,43 @@ render(<Form schema={schema} validator={validator} />, document.getElementById('
2929
```
3030

3131
Note that this library only supports local definition referencing. The value in the `$ref` keyword should be a [JSON Pointer](https://tools.ietf.org/html/rfc6901) in URI fragment identifier format.
32+
33+
## uiSchema for Schema Definitions
34+
35+
To customize the UI for schemas referenced via `$ref`, use the `ui:definitions` property in your uiSchema. This works for both reused and recursive schemas.
36+
37+
See [ui:definitions](../api-reference/uiSchema.md#uidefinitions) for full details and more examples.
38+
39+
```tsx
40+
import { RJSFSchema, UiSchema } from '@rjsf/utils';
41+
import validator from '@rjsf/validator-ajv8';
42+
43+
const schema: RJSFSchema = {
44+
definitions: {
45+
node: {
46+
type: 'object',
47+
properties: {
48+
name: { type: 'string' },
49+
children: {
50+
type: 'array',
51+
items: { $ref: '#/definitions/node' },
52+
},
53+
},
54+
},
55+
},
56+
type: 'object',
57+
properties: {
58+
tree: { $ref: '#/definitions/node' },
59+
},
60+
};
61+
62+
const uiSchema: UiSchema = {
63+
'ui:definitions': {
64+
'#/definitions/node': {
65+
name: { 'ui:placeholder': 'Node name' },
66+
},
67+
},
68+
};
69+
70+
render(<Form schema={schema} uiSchema={uiSchema} validator={validator} />, document.getElementById('app'));
71+
```

packages/playground/src/samples/references.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,20 @@ const references: Sample = {
2424
},
2525
},
2626
},
27+
personType: {
28+
type: 'object',
29+
properties: {
30+
name: { type: 'string' },
31+
details: { type: 'string' },
32+
},
33+
},
34+
companyType: {
35+
type: 'object',
36+
properties: {
37+
name: { type: 'string' },
38+
details: { type: 'string' },
39+
},
40+
},
2741
},
2842
type: 'object',
2943
properties: {
@@ -39,10 +53,66 @@ const references: Sample = {
3953
title: 'Recursive references',
4054
$ref: '#/definitions/node',
4155
},
56+
contact: {
57+
title: 'Contact (oneOf with same property names)',
58+
oneOf: [
59+
{ title: 'Person', $ref: '#/definitions/personType' },
60+
{ title: 'Company', $ref: '#/definitions/companyType' },
61+
],
62+
},
4263
},
4364
},
4465
uiSchema: {
45-
'ui:order': ['shipping_address', 'billing_address', 'tree'],
66+
'ui:order': ['shipping_address', 'billing_address', 'contact', 'tree'],
67+
'ui:definitions': {
68+
'#/definitions/node': {
69+
name: {
70+
'ui:placeholder': 'Enter node name',
71+
'ui:help': 'This UI is defined once in ui:definitions and applied at all recursion levels',
72+
},
73+
children: {
74+
'ui:options': {
75+
orderable: false,
76+
},
77+
},
78+
},
79+
'#/definitions/address': {
80+
street_address: {
81+
'ui:placeholder': 'Street and number',
82+
},
83+
city: {
84+
'ui:placeholder': 'City name',
85+
},
86+
state: {
87+
'ui:placeholder': 'State or region',
88+
},
89+
},
90+
'#/definitions/personType': {
91+
name: {
92+
'ui:placeholder': 'Full name (e.g., John Doe)',
93+
'ui:help': 'Person-specific UI from ui:definitions',
94+
},
95+
details: {
96+
'ui:widget': 'textarea',
97+
'ui:placeholder': 'Personal bio...',
98+
},
99+
},
100+
'#/definitions/companyType': {
101+
name: {
102+
'ui:placeholder': 'Company name (e.g., Acme Inc.)',
103+
'ui:help': 'Company-specific UI from ui:definitions',
104+
},
105+
details: {
106+
'ui:widget': 'textarea',
107+
'ui:placeholder': 'Company description...',
108+
},
109+
},
110+
},
111+
shipping_address: {
112+
street_address: {
113+
'ui:placeholder': 'Shipping street (leave empty for pickup)',
114+
},
115+
},
46116
},
47117
formData: {
48118
billing_address: {
@@ -59,6 +129,10 @@ const references: Sample = {
59129
name: 'root',
60130
children: [{ name: 'leaf' }],
61131
},
132+
contact: {
133+
name: 'Jane Smith',
134+
details: 'Software engineer',
135+
},
62136
},
63137
};
64138

packages/utils/src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const UI_FIELD_KEY = 'ui:field';
4545
export const UI_WIDGET_KEY = 'ui:widget';
4646
export const UI_OPTIONS_KEY = 'ui:options';
4747
export const UI_GLOBAL_OPTIONS_KEY = 'ui:globalOptions';
48+
export const UI_DEFINITIONS_KEY = 'ui:definitions';
4849

4950
/** The JSON Schema version strings
5051
*/

packages/utils/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import pad from './pad';
5858
import parseDateString from './parseDateString';
5959
import rangeSpec from './rangeSpec';
6060
import replaceStringParameters from './replaceStringParameters';
61+
import resolveUiSchema, { expandUiSchemaDefinitions } from './resolveUiSchema';
6162
import schemaRequiresTrueValue from './schemaRequiresTrueValue';
6263
import shouldRender, { ComponentUpdateStrategy } from './shouldRender';
6364
import shouldRenderOptionalField from './shouldRenderOptionalField';
@@ -152,6 +153,8 @@ export {
152153
parseDateString,
153154
rangeSpec,
154155
replaceStringParameters,
156+
resolveUiSchema,
157+
expandUiSchemaDefinitions,
155158
schemaRequiresTrueValue,
156159
shallowEquals,
157160
shouldRender,

0 commit comments

Comments
 (0)