docusaurus-theme-openapi-docs 5.0.2 → 5.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/lib/markdown/schema.js +38 -15
  2. package/lib/markdown/schema.test.d.ts +1 -0
  3. package/lib/markdown/schema.test.js +86 -0
  4. package/lib/theme/ApiExplorer/ApiCodeBlock/Container/index.js +4 -2
  5. package/lib/theme/ApiExplorer/ApiCodeBlock/Content/String.js +9 -6
  6. package/lib/theme/ApiExplorer/ApiCodeBlock/Line/index.d.ts +1 -1
  7. package/lib/theme/ApiExplorer/ApiCodeBlock/index.d.ts +1 -1
  8. package/lib/theme/ApiExplorer/Authorization/index.js +9 -10
  9. package/lib/theme/ApiExplorer/Body/index.js +4 -5
  10. package/lib/theme/ApiExplorer/CodeSnippets/index.js +96 -61
  11. package/lib/theme/ApiExplorer/CodeSnippets/languages.js +12 -1
  12. package/lib/theme/ApiExplorer/CodeSnippets/languages.test.d.ts +1 -0
  13. package/lib/theme/ApiExplorer/CodeSnippets/languages.test.js +102 -0
  14. package/lib/theme/ApiExplorer/CodeTabs/index.d.ts +1 -1
  15. package/lib/theme/ApiExplorer/CodeTabs/index.js +6 -5
  16. package/lib/theme/ApiExplorer/Export/index.js +9 -2
  17. package/lib/theme/ApiExplorer/FormFileUpload/index.js +1 -2
  18. package/lib/theme/ApiExplorer/FormLabel/index.js +1 -2
  19. package/lib/theme/ApiExplorer/FormTextInput/index.js +1 -2
  20. package/lib/theme/ApiExplorer/LiveEditor/index.js +1 -2
  21. package/lib/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamArrayFormItem.js +5 -3
  22. package/lib/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamBooleanFormItem.js +75 -4
  23. package/lib/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamMultiSelectFormItem.js +1 -2
  24. package/lib/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamSelectFormItem.js +67 -4
  25. package/lib/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamTextFormItem.js +65 -1
  26. package/lib/theme/ApiExplorer/ParamOptions/index.js +2 -3
  27. package/lib/theme/ApiExplorer/Request/index.js +17 -18
  28. package/lib/theme/ApiExplorer/Response/index.js +54 -12
  29. package/lib/theme/ApiExplorer/SecuritySchemes/index.js +57 -50
  30. package/lib/theme/ApiExplorer/Server/index.js +2 -3
  31. package/lib/theme/ApiItem/index.js +59 -33
  32. package/lib/theme/ApiTabs/index.d.ts +1 -1
  33. package/lib/theme/ApiTabs/index.js +7 -7
  34. package/lib/theme/DiscriminatorTabs/index.d.ts +1 -1
  35. package/lib/theme/DiscriminatorTabs/index.js +6 -5
  36. package/lib/theme/Example/index.js +3 -4
  37. package/lib/theme/MimeTabs/index.d.ts +1 -1
  38. package/lib/theme/MimeTabs/index.js +6 -5
  39. package/lib/theme/OperationTabs/index.d.ts +1 -1
  40. package/lib/theme/OperationTabs/index.js +6 -5
  41. package/lib/theme/ParamsDetails/index.js +1 -2
  42. package/lib/theme/ParamsItem/index.js +7 -8
  43. package/lib/theme/RequestSchema/index.js +57 -57
  44. package/lib/theme/ResponseExamples/index.js +3 -4
  45. package/lib/theme/ResponseSchema/index.js +26 -24
  46. package/lib/theme/Schema/index.js +148 -110
  47. package/lib/theme/Schema/normalize.d.ts +41 -0
  48. package/lib/theme/Schema/normalize.js +210 -0
  49. package/lib/theme/Schema/normalize.test.d.ts +1 -0
  50. package/lib/theme/Schema/normalize.test.js +271 -0
  51. package/lib/theme/SchemaExpansion/_SchemaExpansion.scss +113 -0
  52. package/lib/theme/SchemaExpansion/context.d.ts +24 -0
  53. package/lib/theme/SchemaExpansion/context.js +187 -0
  54. package/lib/theme/SchemaExpansion/index.d.ts +4 -0
  55. package/lib/theme/SchemaExpansion/index.js +314 -0
  56. package/lib/theme/SchemaItem/index.js +9 -10
  57. package/lib/theme/SchemaTabs/index.d.ts +1 -1
  58. package/lib/theme/SchemaTabs/index.js +6 -5
  59. package/lib/theme/StatusCodes/index.js +2 -4
  60. package/lib/theme/TabItem/index.d.ts +5 -0
  61. package/lib/theme/TabItem/index.js +51 -0
  62. package/lib/theme/TabItem/styles.module.css +3 -0
  63. package/lib/theme/Tabs/index.d.ts +5 -0
  64. package/lib/theme/Tabs/index.js +148 -0
  65. package/lib/theme/Tabs/styles.module.css +7 -0
  66. package/lib/theme/styles.scss +1 -0
  67. package/lib/theme/translationIds.d.ts +1 -93
  68. package/lib/theme/translationIds.js +0 -109
  69. package/lib/theme/utils/codeBlockUtils.d.ts +28 -0
  70. package/lib/theme/utils/codeBlockUtils.js +223 -0
  71. package/lib/theme/utils/reactUtils.d.ts +1 -0
  72. package/lib/theme/utils/reactUtils.js +23 -0
  73. package/lib/theme/utils/scrollUtils.d.ts +7 -0
  74. package/lib/theme/utils/scrollUtils.js +175 -0
  75. package/lib/theme/utils/tabsUtils.d.ts +47 -0
  76. package/lib/theme/utils/tabsUtils.js +299 -0
  77. package/lib/theme/utils/useCodeWordWrap.d.ts +8 -0
  78. package/lib/theme/utils/useCodeWordWrap.js +84 -0
  79. package/lib/theme/utils/useMutationObserver.d.ts +3 -0
  80. package/lib/theme/utils/useMutationObserver.js +34 -0
  81. package/package.json +4 -4
  82. package/src/markdown/schema.test.ts +102 -0
  83. package/src/markdown/schema.ts +42 -15
  84. package/src/theme/ApiExplorer/ApiCodeBlock/Container/index.tsx +2 -1
  85. package/src/theme/ApiExplorer/ApiCodeBlock/Content/String.tsx +8 -7
  86. package/src/theme/ApiExplorer/ApiCodeBlock/Line/index.tsx +1 -1
  87. package/src/theme/ApiExplorer/ApiCodeBlock/index.tsx +1 -1
  88. package/src/theme/ApiExplorer/Authorization/index.tsx +9 -10
  89. package/src/theme/ApiExplorer/Body/index.tsx +7 -5
  90. package/src/theme/ApiExplorer/CodeSnippets/index.tsx +103 -59
  91. package/src/theme/ApiExplorer/CodeSnippets/languages.test.ts +109 -0
  92. package/src/theme/ApiExplorer/CodeSnippets/languages.ts +13 -1
  93. package/src/theme/ApiExplorer/CodeTabs/index.tsx +5 -5
  94. package/src/theme/ApiExplorer/Export/index.tsx +6 -2
  95. package/src/theme/ApiExplorer/FormFileUpload/index.tsx +1 -2
  96. package/src/theme/ApiExplorer/FormLabel/index.tsx +1 -2
  97. package/src/theme/ApiExplorer/FormTextInput/index.tsx +1 -2
  98. package/src/theme/ApiExplorer/LiveEditor/index.tsx +1 -2
  99. package/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamArrayFormItem.tsx +5 -3
  100. package/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamBooleanFormItem.tsx +20 -4
  101. package/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamMultiSelectFormItem.tsx +1 -2
  102. package/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamSelectFormItem.tsx +15 -4
  103. package/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamTextFormItem.tsx +11 -1
  104. package/src/theme/ApiExplorer/ParamOptions/index.tsx +2 -3
  105. package/src/theme/ApiExplorer/Request/index.tsx +23 -18
  106. package/src/theme/ApiExplorer/Response/index.tsx +63 -9
  107. package/src/theme/ApiExplorer/SecuritySchemes/index.tsx +60 -52
  108. package/src/theme/ApiExplorer/Server/index.tsx +8 -3
  109. package/src/theme/ApiItem/index.tsx +43 -21
  110. package/src/theme/ApiTabs/index.tsx +8 -8
  111. package/src/theme/DiscriminatorTabs/index.tsx +6 -5
  112. package/src/theme/Example/index.tsx +3 -4
  113. package/src/theme/MimeTabs/index.tsx +9 -8
  114. package/src/theme/OperationTabs/index.tsx +5 -4
  115. package/src/theme/ParamsDetails/index.tsx +1 -2
  116. package/src/theme/ParamsItem/index.tsx +13 -8
  117. package/src/theme/RequestSchema/index.tsx +38 -40
  118. package/src/theme/ResponseExamples/index.tsx +3 -4
  119. package/src/theme/ResponseSchema/index.tsx +16 -17
  120. package/src/theme/Schema/index.tsx +154 -121
  121. package/src/theme/Schema/normalize.test.ts +288 -0
  122. package/src/theme/Schema/normalize.ts +224 -0
  123. package/src/theme/SchemaExpansion/_SchemaExpansion.scss +113 -0
  124. package/src/theme/SchemaExpansion/context.tsx +154 -0
  125. package/src/theme/SchemaExpansion/index.tsx +236 -0
  126. package/src/theme/SchemaItem/index.tsx +18 -10
  127. package/src/theme/SchemaTabs/index.tsx +6 -5
  128. package/src/theme/StatusCodes/index.tsx +2 -3
  129. package/src/theme/TabItem/index.tsx +61 -0
  130. package/src/theme/TabItem/styles.module.css +3 -0
  131. package/src/theme/Tabs/index.tsx +164 -0
  132. package/src/theme/Tabs/styles.module.css +7 -0
  133. package/src/theme/styles.scss +1 -0
  134. package/src/theme/translationIds.ts +37 -106
  135. package/src/theme/utils/codeBlockUtils.ts +296 -0
  136. package/src/theme/utils/reactUtils.ts +22 -0
  137. package/src/theme/utils/scrollUtils.tsx +153 -0
  138. package/src/theme/utils/tabsUtils.tsx +329 -0
  139. package/src/theme/utils/useCodeWordWrap.ts +110 -0
  140. package/src/theme/utils/useMutationObserver.ts +43 -0
  141. package/src/theme-classic.d.ts +0 -96
  142. package/src/types.d.ts +27 -0
  143. package/tsconfig.tsbuildinfo +1 -1
@@ -5,7 +5,7 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  * ========================================================================== */
7
7
 
8
- import React, { useCallback } from "react";
8
+ import React, { useCallback, useMemo } from "react";
9
9
 
10
10
  import { translate } from "@docusaurus/Translate";
11
11
  import { setSchemaSelection } from "@theme/ApiExplorer/SchemaSelection/slice";
@@ -14,105 +14,28 @@ import { ClosingArrayBracket, OpeningArrayBracket } from "@theme/ArrayBrackets";
14
14
  import Details from "@theme/Details";
15
15
  import DiscriminatorTabs from "@theme/DiscriminatorTabs";
16
16
  import Markdown from "@theme/Markdown";
17
+ import {
18
+ findPropertyDeep,
19
+ foldSiblingsIntoBranches,
20
+ getDiscriminator,
21
+ isCircularMarker,
22
+ mergeAllOf,
23
+ normalizeSchema,
24
+ } from "@theme/Schema/normalize";
25
+ import {
26
+ SchemaDepthProvider,
27
+ useSchemaDepth,
28
+ useSchemaExpansion,
29
+ } from "@theme/SchemaExpansion";
17
30
  import SchemaItem from "@theme/SchemaItem";
18
31
  import SchemaTabs from "@theme/SchemaTabs";
19
32
  import TabItem from "@theme/TabItem";
20
- import { OPENAPI_SCHEMA_ITEM } from "@theme/translationIds";
21
- // eslint-disable-next-line import/no-extraneous-dependencies
22
- import { merge } from "allof-merge";
23
33
  import clsx from "clsx";
24
34
  import isEmpty from "lodash/isEmpty";
25
35
 
26
36
  import { getQualifierMessage, getSchemaName } from "../../markdown/schema";
27
37
  import type { SchemaObject } from "../../types.d";
28
38
 
29
- // eslint-disable-next-line import/no-extraneous-dependencies
30
- // const jsonSchemaMergeAllOf = require("json-schema-merge-allof");
31
-
32
- const mergeAllOf = (allOf: any) => {
33
- const onMergeError = (msg: string) => {
34
- console.warn(msg);
35
- };
36
-
37
- const mergedSchemas = merge(allOf, { onMergeError });
38
-
39
- return mergedSchemas ?? {};
40
- };
41
-
42
- /**
43
- * Recursively searches for a property in a schema, including nested
44
- * oneOf, anyOf, and allOf structures. This is needed for discriminators
45
- * where the property definition may be in a nested schema.
46
- */
47
- const findProperty = (
48
- schema: SchemaObject,
49
- propertyName: string
50
- ): SchemaObject | undefined => {
51
- // Check direct properties first
52
- if (schema.properties?.[propertyName]) {
53
- return schema.properties[propertyName];
54
- }
55
-
56
- // Search in oneOf schemas
57
- if (schema.oneOf) {
58
- for (const subschema of schema.oneOf) {
59
- const found = findProperty(subschema as SchemaObject, propertyName);
60
- if (found) return found;
61
- }
62
- }
63
-
64
- // Search in anyOf schemas
65
- if (schema.anyOf) {
66
- for (const subschema of schema.anyOf) {
67
- const found = findProperty(subschema as SchemaObject, propertyName);
68
- if (found) return found;
69
- }
70
- }
71
-
72
- // Search in allOf schemas
73
- if (schema.allOf) {
74
- for (const subschema of schema.allOf) {
75
- const found = findProperty(subschema as SchemaObject, propertyName);
76
- if (found) return found;
77
- }
78
- }
79
-
80
- return undefined;
81
- };
82
-
83
- /**
84
- * Recursively searches for a discriminator in a schema, including nested
85
- * oneOf, anyOf, and allOf structures.
86
- */
87
- const findDiscriminator = (schema: SchemaObject): any | undefined => {
88
- if (schema.discriminator) {
89
- return schema.discriminator;
90
- }
91
-
92
- if (schema.oneOf) {
93
- for (const subschema of schema.oneOf) {
94
- const found = findDiscriminator(subschema as SchemaObject);
95
- if (found) return found;
96
- }
97
- }
98
-
99
- if (schema.anyOf) {
100
- for (const subschema of schema.anyOf) {
101
- const found = findDiscriminator(subschema as SchemaObject);
102
- if (found) return found;
103
- }
104
- }
105
-
106
- if (schema.allOf) {
107
- for (const subschema of schema.allOf) {
108
- const found = findDiscriminator(subschema as SchemaObject);
109
- if (found) return found;
110
- }
111
- }
112
-
113
- return undefined;
114
- };
115
-
116
39
  interface MarkdownProps {
117
40
  text: string | undefined;
118
41
  }
@@ -165,7 +88,7 @@ const Summary: React.FC<SummaryProps> = ({
165
88
  {nullable && (
166
89
  <span className="openapi-schema__nullable">
167
90
  {translate({
168
- id: OPENAPI_SCHEMA_ITEM.NULLABLE,
91
+ id: "theme.openapi.schemaItem.nullable",
169
92
  message: "nullable",
170
93
  })}
171
94
  </span>
@@ -173,7 +96,7 @@ const Summary: React.FC<SummaryProps> = ({
173
96
  {isRequired && (
174
97
  <span className="openapi-schema__required">
175
98
  {translate({
176
- id: OPENAPI_SCHEMA_ITEM.REQUIRED,
99
+ id: "theme.openapi.schemaItem.required",
177
100
  message: "required",
178
101
  })}
179
102
  </span>
@@ -181,7 +104,7 @@ const Summary: React.FC<SummaryProps> = ({
181
104
  {deprecated && (
182
105
  <span className="openapi-schema__deprecated">
183
106
  {translate({
184
- id: OPENAPI_SCHEMA_ITEM.DEPRECATED,
107
+ id: "theme.openapi.schemaItem.deprecated",
185
108
  message: "deprecated",
186
109
  })}
187
110
  </span>
@@ -218,8 +141,8 @@ const AnyOneOf: React.FC<SchemaProps> = ({
218
141
  }
219
142
 
220
143
  const type = schema.oneOf
221
- ? translate({ id: OPENAPI_SCHEMA_ITEM.ONE_OF, message: "oneOf" })
222
- : translate({ id: OPENAPI_SCHEMA_ITEM.ANY_OF, message: "anyOf" });
144
+ ? translate({ id: "theme.openapi.schemaItem.oneOf", message: "oneOf" })
145
+ : translate({ id: "theme.openapi.schemaItem.anyOf", message: "anyOf" });
223
146
 
224
147
  // Generate a unique ID for this anyOf/oneOf to prevent tab value collisions
225
148
  const uniqueId = React.useMemo(
@@ -268,12 +191,12 @@ const AnyOneOf: React.FC<SchemaProps> = ({
268
191
  if (!label) {
269
192
  if (anyOneSchema.oneOf) {
270
193
  label = translate({
271
- id: OPENAPI_SCHEMA_ITEM.ONE_OF,
194
+ id: "theme.openapi.schemaItem.oneOf",
272
195
  message: "oneOf",
273
196
  });
274
197
  } else if (anyOneSchema.anyOf) {
275
198
  label = translate({
276
- id: OPENAPI_SCHEMA_ITEM.ANY_OF,
199
+ id: "theme.openapi.schemaItem.anyOf",
277
200
  message: "anyOf",
278
201
  });
279
202
  } else {
@@ -293,6 +216,11 @@ const AnyOneOf: React.FC<SchemaProps> = ({
293
216
  label={label}
294
217
  value={`${uniqueId}-${index}-item`}
295
218
  >
219
+ {anyOneSchema.description && (
220
+ <div style={{ marginLeft: "1rem" }}>
221
+ <MarkdownWrapper text={anyOneSchema.description} />
222
+ </div>
223
+ )}
296
224
  {/* Handle primitive types directly */}
297
225
  {(isPrimitive(anyOneSchema) || anyOneSchema.const) && (
298
226
  <SchemaItem
@@ -454,7 +382,7 @@ const PropertyDiscriminator: React.FC<SchemaEdgeProps> = ({
454
382
  {required && (
455
383
  <span className="openapi-schema__required">
456
384
  {translate({
457
- id: OPENAPI_SCHEMA_ITEM.REQUIRED,
385
+ id: "theme.openapi.schemaItem.required",
458
386
  message: "required",
459
387
  })}
460
388
  </span>
@@ -476,6 +404,13 @@ const PropertyDiscriminator: React.FC<SchemaEdgeProps> = ({
476
404
  label={key}
477
405
  value={`${index}-item-discriminator`}
478
406
  >
407
+ {discriminator.mapping[key]?.description && (
408
+ <div style={{ marginLeft: "1rem" }}>
409
+ <MarkdownWrapper
410
+ text={discriminator.mapping[key].description}
411
+ />
412
+ </div>
413
+ )}
479
414
  <SchemaNode
480
415
  schema={discriminator.mapping[key]}
481
416
  schemaType={schemaType}
@@ -521,9 +456,11 @@ const DiscriminatorNode: React.FC<DiscriminatorNodeProps> = ({
521
456
  let discriminatedSchemas: any = {};
522
457
  let inferredMapping: any = {};
523
458
 
524
- // Search for the discriminator property in the schema, including nested structures
459
+ // Search for the discriminator property in the schema, including nested
460
+ // structures. Cached recursive lookup replaces the O(subtree) findProperty
461
+ // walk that caused #1525's O(N^2) render cost.
525
462
  const discriminatorProperty =
526
- findProperty(schema, discriminator.propertyName) ?? {};
463
+ findPropertyDeep(schema, discriminator.propertyName) ?? {};
527
464
 
528
465
  if (schema.allOf) {
529
466
  const mergedSchemas = mergeAllOf(schema) as SchemaObject;
@@ -604,6 +541,20 @@ const AdditionalProperties: React.FC<SchemaProps> = ({
604
541
 
605
542
  if (!additionalProperties) return null;
606
543
 
544
+ if (isCircularMarker(additionalProperties)) {
545
+ return (
546
+ <SchemaItem
547
+ name="property name*"
548
+ required={false}
549
+ schemaName={additionalProperties}
550
+ schema={additionalProperties}
551
+ collapsible={false}
552
+ discriminator={false}
553
+ children={null}
554
+ />
555
+ );
556
+ }
557
+
607
558
  // Handle free-form objects
608
559
  if (additionalProperties === true || isEmpty(additionalProperties)) {
609
560
  return (
@@ -676,10 +627,15 @@ const SchemaNodeDetails: React.FC<SchemaEdgeProps> = ({
676
627
  schemaType,
677
628
  schemaPath,
678
629
  }) => {
630
+ const depth = useSchemaDepth();
631
+ const { level } = useSchemaExpansion();
632
+ const defaultOpen = depth < level;
679
633
  return (
680
634
  <SchemaItem collapsible={true}>
681
635
  <Details
636
+ key={`level-${level}`}
682
637
  className="openapi-markdown__details"
638
+ open={defaultOpen}
683
639
  summary={
684
640
  <Summary
685
641
  name={name}
@@ -694,11 +650,13 @@ const SchemaNodeDetails: React.FC<SchemaEdgeProps> = ({
694
650
  {getQualifierMessage(schema) && (
695
651
  <MarkdownWrapper text={getQualifierMessage(schema)} />
696
652
  )}
697
- <SchemaNode
698
- schema={schema}
699
- schemaType={schemaType}
700
- schemaPath={schemaPath}
701
- />
653
+ <SchemaDepthProvider depth={depth + 1}>
654
+ <SchemaNode
655
+ schema={schema}
656
+ schemaType={schemaType}
657
+ schemaPath={schemaPath}
658
+ />
659
+ </SchemaDepthProvider>
702
660
  </div>
703
661
  </Details>
704
662
  </SchemaItem>
@@ -710,6 +668,23 @@ const Items: React.FC<{
710
668
  schemaType: "request" | "response";
711
669
  schemaPath?: string;
712
670
  }> = ({ schema, schemaType, schemaPath }) => {
671
+ if (isCircularMarker(schema.items)) {
672
+ return (
673
+ <div style={{ marginLeft: ".5rem" }}>
674
+ <OpeningArrayBracket />
675
+ <SchemaItem
676
+ collapsible={false}
677
+ name=""
678
+ schemaName={schema.items}
679
+ schema={schema.items}
680
+ discriminator={false}
681
+ children={null}
682
+ />
683
+ <ClosingArrayBracket />
684
+ </div>
685
+ );
686
+ }
687
+
713
688
  // Process schema.items to handle allOf merging
714
689
  let itemsSchema = schema.items;
715
690
  if (schema.items?.allOf) {
@@ -725,17 +700,25 @@ const Items: React.FC<{
725
700
  const itemsSchemaPath = schemaPath ? `${schemaPath}.items` : undefined;
726
701
 
727
702
  if (hasOneOfAnyOf || hasProperties || hasAdditionalProperties) {
703
+ // Fold sibling properties into each oneOf/anyOf branch to avoid duplicate
704
+ // property rendering. See issue #1218.
705
+ const renderSchema =
706
+ hasOneOfAnyOf && hasProperties
707
+ ? foldSiblingsIntoBranches(itemsSchema)
708
+ : itemsSchema;
709
+ const renderHasProperties =
710
+ hasOneOfAnyOf && hasProperties ? false : hasProperties;
728
711
  return (
729
712
  <>
730
713
  <OpeningArrayBracket />
731
714
  {hasOneOfAnyOf && (
732
715
  <AnyOneOf
733
- schema={itemsSchema}
716
+ schema={renderSchema}
734
717
  schemaType={schemaType}
735
718
  schemaPath={itemsSchemaPath}
736
719
  />
737
720
  )}
738
- {hasProperties && (
721
+ {renderHasProperties && (
739
722
  <Properties
740
723
  schema={itemsSchema}
741
724
  schemaType={schemaType}
@@ -822,6 +805,20 @@ const SchemaEdge: React.FC<SchemaEdgeProps> = ({
822
805
  return null;
823
806
  }
824
807
 
808
+ if (isCircularMarker(schema)) {
809
+ return (
810
+ <SchemaItem
811
+ collapsible={false}
812
+ name={name}
813
+ required={Array.isArray(required) ? required.includes(name) : required}
814
+ schemaName={schema}
815
+ schema={schema}
816
+ discriminator={false}
817
+ children={null}
818
+ />
819
+ );
820
+ }
821
+
825
822
  const schemaName = getSchemaName(schema);
826
823
 
827
824
  if (discriminator && discriminator.propertyName === name) {
@@ -908,6 +905,20 @@ const SchemaEdge: React.FC<SchemaEdgeProps> = ({
908
905
  );
909
906
  }
910
907
 
908
+ if (isCircularMarker(schema.items)) {
909
+ return (
910
+ <SchemaNodeDetails
911
+ name={name}
912
+ schemaName={schemaName}
913
+ required={required}
914
+ nullable={schema.nullable}
915
+ schema={schema}
916
+ schemaType={schemaType}
917
+ schemaPath={schemaPath}
918
+ />
919
+ );
920
+ }
921
+
911
922
  if (schema.allOf) {
912
923
  // handle circular properties
913
924
  if (
@@ -1020,23 +1031,31 @@ function renderChildren(
1020
1031
  schemaType: "request" | "response",
1021
1032
  schemaPath?: string
1022
1033
  ) {
1034
+ // Fold sibling properties into each oneOf/anyOf branch to avoid duplicate
1035
+ // property rendering. See issue #1218.
1036
+ const hasOneOfAnyOf = !!(schema.oneOf || schema.anyOf);
1037
+ const hasProperties = !!schema.properties;
1038
+ const folded =
1039
+ hasOneOfAnyOf && hasProperties ? foldSiblingsIntoBranches(schema) : schema;
1040
+ const renderProperties =
1041
+ hasOneOfAnyOf && hasProperties ? false : hasProperties;
1023
1042
  return (
1024
1043
  <>
1025
- {schema.oneOf && (
1044
+ {folded.oneOf && (
1026
1045
  <AnyOneOf
1027
- schema={schema}
1046
+ schema={folded}
1028
1047
  schemaType={schemaType}
1029
1048
  schemaPath={schemaPath}
1030
1049
  />
1031
1050
  )}
1032
- {schema.anyOf && (
1051
+ {folded.anyOf && (
1033
1052
  <AnyOneOf
1034
- schema={schema}
1053
+ schema={folded}
1035
1054
  schemaType={schemaType}
1036
1055
  schemaPath={schemaPath}
1037
1056
  />
1038
1057
  )}
1039
- {schema.properties && (
1058
+ {renderProperties && (
1040
1059
  <Properties
1041
1060
  schema={schema}
1042
1061
  schemaType={schemaType}
@@ -1058,10 +1077,12 @@ function renderChildren(
1058
1077
  }
1059
1078
 
1060
1079
  const SchemaNode: React.FC<SchemaProps> = ({
1061
- schema,
1080
+ schema: rawSchema,
1062
1081
  schemaType,
1063
1082
  schemaPath,
1064
1083
  }) => {
1084
+ const schema = useMemo(() => normalizeSchema(rawSchema), [rawSchema]);
1085
+
1065
1086
  if (
1066
1087
  (schemaType === "request" && schema.readOnly) ||
1067
1088
  (schemaType === "response" && schema.writeOnly)
@@ -1070,10 +1091,11 @@ const SchemaNode: React.FC<SchemaProps> = ({
1070
1091
  }
1071
1092
 
1072
1093
  // Resolve discriminator recursively so nested oneOf/anyOf/allOf compositions
1073
- // can still render discriminator tabs.
1094
+ // can still render discriminator tabs. Cached via getDiscriminator so per-
1095
+ // render cost is O(1) amortized (see #1525).
1074
1096
  let workingSchema = schema;
1075
1097
  const resolvedDiscriminator =
1076
- schema.discriminator ?? findDiscriminator(schema);
1098
+ schema.discriminator ?? getDiscriminator(schema);
1077
1099
  if (schema.allOf && !schema.discriminator && resolvedDiscriminator) {
1078
1100
  workingSchema = mergeAllOf(schema) as SchemaObject;
1079
1101
  }
@@ -1160,23 +1182,34 @@ const SchemaNode: React.FC<SchemaProps> = ({
1160
1182
  return null;
1161
1183
  }
1162
1184
 
1185
+ // Fold sibling properties into each oneOf/anyOf branch to avoid duplicate
1186
+ // property rendering. See issue #1218.
1187
+ const hasOneOfAnyOf = !!(mergedSchemas.oneOf || mergedSchemas.anyOf);
1188
+ const hasProperties = !!mergedSchemas.properties;
1189
+ const folded =
1190
+ hasOneOfAnyOf && hasProperties
1191
+ ? foldSiblingsIntoBranches(mergedSchemas)
1192
+ : mergedSchemas;
1193
+ const renderProperties =
1194
+ hasOneOfAnyOf && hasProperties ? false : hasProperties;
1195
+
1163
1196
  return (
1164
1197
  <div>
1165
- {mergedSchemas.oneOf && (
1198
+ {folded.oneOf && (
1166
1199
  <AnyOneOf
1167
- schema={mergedSchemas}
1200
+ schema={folded}
1168
1201
  schemaType={schemaType}
1169
1202
  schemaPath={schemaPath}
1170
1203
  />
1171
1204
  )}
1172
- {mergedSchemas.anyOf && (
1205
+ {folded.anyOf && (
1173
1206
  <AnyOneOf
1174
- schema={mergedSchemas}
1207
+ schema={folded}
1175
1208
  schemaType={schemaType}
1176
1209
  schemaPath={schemaPath}
1177
1210
  />
1178
1211
  )}
1179
- {mergedSchemas.properties && (
1212
+ {renderProperties && (
1180
1213
  <Properties
1181
1214
  schema={mergedSchemas}
1182
1215
  schemaType={schemaType}