Skip to content

Commit a681ff1

Browse files
Stop infinite renders caused by circular $ref object properties
Fixes #3907, #4262 and #3826 - Adds RJSF_REF_CYCLE_KEY ('__rjsf_ref_cycle') to detect and terminate self-referential schemas that would otherwise render infinitely. - In resolveAllReferences, when a $ref is encountered that's already in the recurseList AND we're resolving in simple (non-resolveAnyOfOrOneOfRefs) mode, the schema is tagged with __rjsf_ref_cycle: true instead of silently returning the unresolved $ref. This only fires in a direct object-property context (!resolveAnyOfOrOneOfRefs), where rendering is unconditional and cycles are genuinely infinite. Array-items and anyOf/oneOf contexts remain untagged since they are data-driven and terminate naturally. - SchemaField checks _schema[RJSF_REF_CYCLE_KEY] after its useCallback hook (to satisfy React rules of hooks) and renders a CyclicSchemaField instead of recursing. CyclicSchemaField renders the CyclicSchemaExpandTemplate, which shows the user a message and an expand button to load the next cycle break. - hashForSchema now filters out keys starting with RJSF_REF_KEY before hashing so that internal cycle-tracking metadata does not affect the computed hash. - CyclicSchemaExpandTemplate is implemented for all theme packages: antd, chakra-ui, daisyui, fluentui-rc, mantine, mui, primereact, react-bootstrap, semantic-ui, and shadcn, each using their native UI library components. - Fixed buttonId in all CyclicSchemaExpandTemplate implementations to use fieldPathId[ID_KEY] (the $id string) instead of fieldPathId (the object). Button id format is now `${fieldPathId.$id}-button` (e.g. `root_child_child-button`), dropping the redundant `-${name}-` segment. - Added SchemaField test verifying that clicking the expand button renders the next level of the cycle, finding the button by its correct DOM id. - Updated FormSnap snapshot to reflect the corrected button id. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 96baf8d commit a681ff1

68 files changed

Lines changed: 3692 additions & 833 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,65 @@ should change the heading of the (upcoming) version to include a major version b
1616
1717
-->
1818

19+
# 6.7.0
20+
21+
## @rjsf/antd
22+
23+
- Added `CyclicSchemaExpandTemplate` to the list of templates for the theme, updating snapshots accordingly
24+
25+
## @rjsf/chakra-ui
26+
27+
- Added `CyclicSchemaExpandTemplate` to the list of templates for the theme, updating snapshots accordingly
28+
29+
## @rjsf/core
30+
31+
- Fixed [#3907](https://github.com/rjsf-team/react-jsonschema-form/issues/3907) and [#4262](https://github.com/rjsf-team/react-jsonschema-form/issues/4262) as follows:
32+
- Added `CyclicSchemaExpandTemplate` to the list of templates for the theme, updating snapshots accordingly
33+
- Added `CyclicSchemaField` to the list of fields, that renders the `CyclicSchemaExpandTemplate` initially and, if expanded, will render the `SchemaField` with the `RJSF_REF_CYCLE_KEY` tag turned off
34+
- Updated `SchemaForm` to render the `CyclicSchemaField` when the schema contains the `RJSF_REF_CYCLE_KEY` set to `true`
35+
36+
## @rjsf/daisyui
37+
38+
- Added `CyclicSchemaExpandTemplate` to the list of templates for the theme, updating snapshots accordingly
39+
40+
## @rjsf/fluentui-rc
41+
42+
- Added `CyclicSchemaExpandTemplate` to the list of templates for the theme, updating snapshots accordingly
43+
44+
## @rjsf/mantine
45+
46+
- Added `CyclicSchemaExpandTemplate` to the list of templates for the theme, updating snapshots accordingly
47+
48+
## @rjsf/mui
49+
50+
- Added `CyclicSchemaExpandTemplate` to the list of templates for the theme, updating snapshots accordingly
51+
52+
## @rjsf/primereact
53+
54+
- Added `CyclicSchemaExpandTemplate` to the list of templates for the theme, updating snapshots accordingly
55+
56+
## @rjsf/react-bootstrap
57+
58+
- Added `CyclicSchemaExpandTemplate` to the list of templates for the theme, updating snapshots accordingly
59+
60+
## @rjsf/semantic-ui
61+
62+
- Added `CyclicSchemaExpandTemplate` to the list of templates for the theme, updating snapshots accordingly
63+
64+
## @rjsf/shadcn
65+
66+
- Added `CyclicSchemaExpandTemplate` to the list of templates for the theme, updating snapshots accordingly
67+
68+
## @rjsf/utils
69+
70+
- Updated `types.ts` to add `CyclicSchemaExpandProps` type and `CyclicSchemaExpandTemplate` in the `TemplatesType`
71+
- Updated `resolveAllReferences()` to add a new `markCycleOnDetection` prop which adds `RJSF_REF_CYCLE_KEY` marker (from `constants.ts`) to a schema that has been detected to have a cycle, partially fixing [#3907](https://github.com/rjsf-team/react-jsonschema-form/issues/3907)
72+
- Updated `hashForSchema()` to filter keys to remove `RJSF_REF_KEY` prefixed keys before hashing the schema
73+
- Updated `enums.ts` to add `ExpandButton` and `CycleDetected` keys
74+
75+
## Dev / docs / playground
76+
77+
- Updated `@rjsf/snapshots` to add a test case to `formTests` that verifies the new Cycle detection UI
1978
# 6.6.2
2079

2180
## @rjsf/core
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {
2+
CyclicSchemaExpandProps,
3+
FormContextType,
4+
ID_KEY,
5+
RJSFSchema,
6+
StrictRJSFSchema,
7+
TranslatableString,
8+
} from '@rjsf/utils';
9+
import { Alert, Button, Space, version } from 'antd';
10+
11+
const antdMajor = parseInt(version.split('.')[0], 10);
12+
13+
/** The `CyclicSchemaExpandTemplate` is the template to use to render the cyclic schema expand message and controls
14+
*
15+
* @param props - The `CyclicSchemaExpandProps` for this component
16+
*/
17+
export default function CyclicSchemaExpandTemplate<
18+
T = any,
19+
S extends StrictRJSFSchema = RJSFSchema,
20+
F extends FormContextType = any,
21+
>(props: CyclicSchemaExpandProps<T, S, F>) {
22+
const { name, fieldPathId, registry, onExpand } = props;
23+
const { translateString } = registry;
24+
const buttonId = `${fieldPathId[ID_KEY]}-button`;
25+
26+
const headerProp =
27+
antdMajor >= 6
28+
? { title: translateString(TranslatableString.CycleDetected, [name]) }
29+
: { message: translateString(TranslatableString.CycleDetected, [name]) };
30+
31+
return (
32+
<Alert
33+
type='warning'
34+
{...headerProp}
35+
action={
36+
<Space>
37+
<Button id={buttonId} size='small' type='default' onClick={() => onExpand(fieldPathId[ID_KEY])}>
38+
{translateString(TranslatableString.ExpandButton)}
39+
</Button>
40+
</Space>
41+
}
42+
/>
43+
);
44+
}

packages/antd/src/templates/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { FormContextType, RJSFSchema, StrictRJSFSchema, TemplatesType } from '@r
33
import ArrayFieldItemTemplate from './ArrayFieldItemTemplate';
44
import ArrayFieldTemplate from './ArrayFieldTemplate';
55
import BaseInputTemplate from './BaseInputTemplate';
6+
import CyclicSchemaExpandTemplate from './CyclicSchemaExpandTemplate';
67
import ErrorList from './ErrorList';
78
import DescriptionField from './FieldDescriptionTemplate';
89
import FieldErrorTemplate from './FieldErrorTemplate';
@@ -25,6 +26,7 @@ export function generateTemplates<
2526
ArrayFieldItemTemplate,
2627
ArrayFieldTemplate,
2728
BaseInputTemplate,
29+
CyclicSchemaExpandTemplate,
2830
ButtonTemplates: {
2931
AddButton,
3032
CopyButton,

packages/antd/test/__snapshots__/Form.test.tsx.snap

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4547,6 +4547,195 @@ exports[`nameGenerator > dotNotationNameGenerator > simple fields 1`] = `
45474547
</DocumentFragment>
45484548
`;
45494549

4550+
exports[`single fields > Cyclic schema 1`] = `
4551+
<DocumentFragment>
4552+
<form
4553+
class="rjsf"
4554+
>
4555+
<div
4556+
class="rjsf-field rjsf-field-object"
4557+
>
4558+
<div
4559+
class="ant-form-item css-var-root ant-form-css-var css-dev-only-do-not-override-ypkju9 ant-form-item-horizontal"
4560+
>
4561+
<div
4562+
class="ant-row ant-form-item-row css-dev-only-do-not-override-ypkju9 css-var-root"
4563+
>
4564+
<div
4565+
class="ant-col ant-col-24 ant-form-item-control css-dev-only-do-not-override-ypkju9 css-var-root"
4566+
>
4567+
<div
4568+
class="ant-form-item-control-input"
4569+
>
4570+
<div
4571+
class="ant-form-item-control-input-content"
4572+
>
4573+
<fieldset
4574+
id="root"
4575+
>
4576+
<div
4577+
class="ant-row css-dev-only-do-not-override-ypkju9 css-var-root"
4578+
style="margin-inline: -12px;"
4579+
>
4580+
<div
4581+
class="ant-col ant-col-24 ant-form-item-label css-dev-only-do-not-override-ypkju9 css-var-root"
4582+
style="padding-inline: 12px;"
4583+
>
4584+
<label
4585+
class=""
4586+
for="root__title"
4587+
title="A registration form"
4588+
>
4589+
A registration form
4590+
</label>
4591+
<div
4592+
class="ant-divider css-dev-only-do-not-override-ypkju9 css-var-root ant-divider-horizontal ant-divider-sm ant-divider-rail"
4593+
role="separator"
4594+
style="margin-block: 1px;"
4595+
/>
4596+
</div>
4597+
<div
4598+
class="ant-col ant-col-24 css-dev-only-do-not-override-ypkju9 css-var-root"
4599+
style="padding-inline: 12px;"
4600+
>
4601+
<div
4602+
class="rjsf-field rjsf-field-object"
4603+
>
4604+
<div
4605+
class="ant-form-item css-var-root ant-form-css-var css-dev-only-do-not-override-ypkju9 ant-form-item-horizontal"
4606+
>
4607+
<div
4608+
class="ant-row ant-form-item-row css-dev-only-do-not-override-ypkju9 css-var-root"
4609+
>
4610+
<div
4611+
class="ant-col ant-col-24 ant-form-item-control css-dev-only-do-not-override-ypkju9 css-var-root"
4612+
>
4613+
<div
4614+
class="ant-form-item-control-input"
4615+
>
4616+
<div
4617+
class="ant-form-item-control-input-content"
4618+
>
4619+
<fieldset
4620+
id="root_child"
4621+
>
4622+
<div
4623+
class="ant-row css-dev-only-do-not-override-ypkju9 css-var-root"
4624+
style="margin-inline: -12px;"
4625+
>
4626+
<div
4627+
class="ant-col ant-col-24 ant-form-item-label css-dev-only-do-not-override-ypkju9 css-var-root"
4628+
style="padding-inline: 12px;"
4629+
>
4630+
<label
4631+
class=""
4632+
for="root_child__title"
4633+
title="A registration form"
4634+
>
4635+
A registration form
4636+
</label>
4637+
<div
4638+
class="ant-divider css-dev-only-do-not-override-ypkju9 css-var-root ant-divider-horizontal ant-divider-sm ant-divider-rail"
4639+
role="separator"
4640+
style="margin-block: 1px;"
4641+
/>
4642+
</div>
4643+
<div
4644+
class="ant-col ant-col-24 css-dev-only-do-not-override-ypkju9 css-var-root"
4645+
style="padding-inline: 12px;"
4646+
>
4647+
<div
4648+
class="ant-alert ant-alert-warning ant-alert-outlined ant-alert-no-icon css-var-root css-dev-only-do-not-override-ypkju9"
4649+
data-show="true"
4650+
role="alert"
4651+
>
4652+
<div
4653+
class="ant-alert-section"
4654+
>
4655+
<div
4656+
class="ant-alert-title"
4657+
>
4658+
Circular reference ($ref cycle) detected for field "child". You may choose to expand to the next cycle break
4659+
</div>
4660+
</div>
4661+
<div
4662+
class="ant-alert-actions"
4663+
>
4664+
<div
4665+
class="ant-space css-dev-only-do-not-override-ypkju9 ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small css-var-root"
4666+
>
4667+
<div
4668+
class="ant-space-item"
4669+
>
4670+
<button
4671+
class="ant-btn css-dev-only-do-not-override-ypkju9 css-var-root ant-btn-default ant-btn-color-default ant-btn-variant-outlined ant-btn-sm"
4672+
id="root_child_child-button"
4673+
type="button"
4674+
>
4675+
<span>
4676+
Expand Cycle
4677+
</span>
4678+
</button>
4679+
</div>
4680+
</div>
4681+
</div>
4682+
</div>
4683+
</div>
4684+
</div>
4685+
</fieldset>
4686+
</div>
4687+
</div>
4688+
<div
4689+
class="ant-form-item-additional"
4690+
>
4691+
<div
4692+
class="ant-form-item-extra"
4693+
>
4694+
<span
4695+
id="root_child__description"
4696+
>
4697+
A simple form example.
4698+
</span>
4699+
</div>
4700+
</div>
4701+
</div>
4702+
</div>
4703+
</div>
4704+
</div>
4705+
</div>
4706+
</div>
4707+
</fieldset>
4708+
</div>
4709+
</div>
4710+
<div
4711+
class="ant-form-item-additional"
4712+
>
4713+
<div
4714+
class="ant-form-item-extra"
4715+
>
4716+
<span
4717+
id="root__description"
4718+
>
4719+
A simple form example.
4720+
</span>
4721+
</div>
4722+
</div>
4723+
</div>
4724+
</div>
4725+
</div>
4726+
</div>
4727+
<button
4728+
class="ant-btn css-dev-only-do-not-override-ypkju9 css-var-root ant-btn-submit"
4729+
type="submit"
4730+
>
4731+
<span>
4732+
Submit
4733+
</span>
4734+
</button>
4735+
</form>
4736+
</DocumentFragment>
4737+
`;
4738+
45504739
exports[`single fields > checkbox field 1`] = `
45514740
<DocumentFragment>
45524741
<form
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Box, Button } from '@chakra-ui/react';
2+
import {
3+
CyclicSchemaExpandProps,
4+
FormContextType,
5+
ID_KEY,
6+
RJSFSchema,
7+
StrictRJSFSchema,
8+
TranslatableString,
9+
} from '@rjsf/utils';
10+
11+
import { Alert } from '../components/ui/alert';
12+
13+
/** The `CyclicSchemaExpandTemplate` is the template to use to render the cyclic schema expand message and controls
14+
*
15+
* @param props - The `CyclicSchemaExpandProps` for this component
16+
*/
17+
export default function CyclicSchemaExpandTemplate<
18+
T = any,
19+
S extends StrictRJSFSchema = RJSFSchema,
20+
F extends FormContextType = any,
21+
>(props: CyclicSchemaExpandProps<T, S, F>) {
22+
const { name, fieldPathId, registry, onExpand } = props;
23+
const { translateString } = registry;
24+
const buttonId = `${fieldPathId[ID_KEY]}-button`;
25+
return (
26+
<Box mt={4}>
27+
<Alert status='warning' title={translateString(TranslatableString.CycleDetected, [name])} mb={2} />
28+
<Button id={buttonId} size='sm' variant='outline' onClick={() => onExpand(fieldPathId[ID_KEY])}>
29+
{translateString(TranslatableString.ExpandButton)}
30+
</Button>
31+
</Box>
32+
);
33+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from './CyclicSchemaExpandTemplate';
2+
export * from './CyclicSchemaExpandTemplate';

packages/chakra-ui/src/Templates/Templates.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import AddButton from '../AddButton';
44
import ArrayFieldItemTemplate from '../ArrayFieldItemTemplate';
55
import ArrayFieldTemplate from '../ArrayFieldTemplate';
66
import BaseInputTemplate from '../BaseInputTemplate/BaseInputTemplate';
7+
import CyclicSchemaExpandTemplate from '../CyclicSchemaExpandTemplate';
78
import DescriptionField from '../DescriptionField';
89
import ErrorList from '../ErrorList';
910
import FieldErrorTemplate from '../FieldErrorTemplate';
@@ -27,6 +28,7 @@ export function generateTemplates<
2728
ArrayFieldItemTemplate,
2829
ArrayFieldTemplate,
2930
BaseInputTemplate,
31+
CyclicSchemaExpandTemplate,
3032
ButtonTemplates: {
3133
CopyButton,
3234
AddButton,

0 commit comments

Comments
 (0)