docusaurus-theme-openapi-docs 0.0.0-1314 → 0.0.0-1332

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.
@@ -0,0 +1,271 @@
1
+ "use strict";
2
+ /* ============================================================================
3
+ * Copyright (c) Palo Alto Networks
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ * ========================================================================== */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ const normalize_1 = require("./normalize");
10
+ describe("normalizeSchema", () => {
11
+ it("returns the input as-is (identity-stable pass-through)", () => {
12
+ const schema = { type: "object", properties: { a: { type: "string" } } };
13
+ expect((0, normalize_1.normalizeSchema)(schema)).toBe(schema);
14
+ });
15
+ it("handles null, undefined, and primitives", () => {
16
+ expect((0, normalize_1.normalizeSchema)(null)).toBeNull();
17
+ expect((0, normalize_1.normalizeSchema)(undefined)).toBeUndefined();
18
+ expect((0, normalize_1.normalizeSchema)("string")).toBe("string");
19
+ expect((0, normalize_1.normalizeSchema)(42)).toBe(42);
20
+ });
21
+ });
22
+ describe("mergeAllOf", () => {
23
+ it("merges allOf members into a flat schema", () => {
24
+ const schema = {
25
+ allOf: [
26
+ { type: "object", properties: { a: { type: "string" } } },
27
+ { type: "object", properties: { b: { type: "number" } } },
28
+ ],
29
+ };
30
+ const result = (0, normalize_1.mergeAllOf)(schema);
31
+ expect(result.allOf).toBeUndefined();
32
+ expect(result.properties.a).toEqual({ type: "string" });
33
+ expect(result.properties.b).toEqual({ type: "number" });
34
+ });
35
+ it("strips conflicting additionalProperties: false from allOf members", () => {
36
+ const schema = {
37
+ allOf: [
38
+ {
39
+ type: "object",
40
+ properties: { a: { type: "string" } },
41
+ additionalProperties: false,
42
+ },
43
+ {
44
+ type: "object",
45
+ properties: { b: { type: "number" } },
46
+ additionalProperties: false,
47
+ },
48
+ ],
49
+ };
50
+ const result = (0, normalize_1.mergeAllOf)(schema);
51
+ expect(result.properties.a).toBeDefined();
52
+ expect(result.properties.b).toBeDefined();
53
+ });
54
+ it("preserves both properties and oneOf when merging incompatible-shape members", () => {
55
+ const result = (0, normalize_1.mergeAllOf)({
56
+ allOf: [
57
+ { type: "object", properties: { id: { type: "string" } } },
58
+ { oneOf: [{ title: "a" }, { title: "b" }] },
59
+ ],
60
+ });
61
+ expect(result.properties.id).toBeDefined();
62
+ expect(result.oneOf).toHaveLength(2);
63
+ });
64
+ });
65
+ describe("foldSiblingsIntoBranches", () => {
66
+ it("returns schema unchanged when no oneOf/anyOf is present", () => {
67
+ const schema = { type: "object", properties: { a: { type: "string" } } };
68
+ expect((0, normalize_1.foldSiblingsIntoBranches)(schema)).toBe(schema);
69
+ });
70
+ it("returns schema unchanged when branches have no siblings", () => {
71
+ const schema = {
72
+ oneOf: [{ properties: { a: { type: "string" } } }],
73
+ };
74
+ expect((0, normalize_1.foldSiblingsIntoBranches)(schema)).toBe(schema);
75
+ });
76
+ it("returns schema unchanged when a discriminator is present", () => {
77
+ const schema = {
78
+ discriminator: { propertyName: "type" },
79
+ properties: { shared: { type: "string" } },
80
+ oneOf: [{ properties: { a: { type: "number" } } }],
81
+ };
82
+ expect((0, normalize_1.foldSiblingsIntoBranches)(schema)).toBe(schema);
83
+ });
84
+ it("folds sibling properties into oneOf branches", () => {
85
+ const schema = {
86
+ properties: { shared: { type: "string" } },
87
+ oneOf: [
88
+ { properties: { a: { type: "number" } } },
89
+ { properties: { b: { type: "boolean" } } },
90
+ ],
91
+ };
92
+ const result = (0, normalize_1.foldSiblingsIntoBranches)(schema);
93
+ expect(result.properties).toBeUndefined();
94
+ expect(result.oneOf).toHaveLength(2);
95
+ expect(result.oneOf[0].properties.shared).toBeDefined();
96
+ expect(result.oneOf[0].properties.a).toBeDefined();
97
+ });
98
+ it("folds sibling properties into anyOf branches", () => {
99
+ const schema = {
100
+ properties: { shared: { type: "string" } },
101
+ anyOf: [
102
+ { properties: { a: { type: "number" } } },
103
+ { properties: { b: { type: "boolean" } } },
104
+ ],
105
+ };
106
+ const result = (0, normalize_1.foldSiblingsIntoBranches)(schema);
107
+ expect(result.properties).toBeUndefined();
108
+ expect(result.anyOf).toHaveLength(2);
109
+ expect(result.anyOf[1].properties.shared).toBeDefined();
110
+ expect(result.anyOf[1].properties.b).toBeDefined();
111
+ });
112
+ it("preserves metadata and x-extension keys when folding", () => {
113
+ const schema = {
114
+ title: "Test",
115
+ description: "desc",
116
+ deprecated: true,
117
+ "x-vendor": true,
118
+ properties: { shared: { type: "string" } },
119
+ anyOf: [{ properties: { a: { type: "number" } } }],
120
+ };
121
+ const result = (0, normalize_1.foldSiblingsIntoBranches)(schema);
122
+ expect(result.title).toBe("Test");
123
+ expect(result.description).toBe("desc");
124
+ expect(result.deprecated).toBe(true);
125
+ expect(result["x-vendor"]).toBe(true);
126
+ expect(result.properties).toBeUndefined();
127
+ });
128
+ it("strips non-metadata keys from result", () => {
129
+ const schema = {
130
+ type: "object",
131
+ required: ["shared"],
132
+ properties: { shared: { type: "string" } },
133
+ items: { type: "string" },
134
+ oneOf: [{ properties: { a: { type: "number" } } }],
135
+ };
136
+ const result = (0, normalize_1.foldSiblingsIntoBranches)(schema);
137
+ expect(result.type).toBeUndefined();
138
+ expect(result.required).toBeUndefined();
139
+ expect(result.properties).toBeUndefined();
140
+ expect(result.items).toBeUndefined();
141
+ expect(result.oneOf).toHaveLength(1);
142
+ });
143
+ });
144
+ describe("getDiscriminator", () => {
145
+ it("returns direct discriminator", () => {
146
+ const schema = { discriminator: { propertyName: "type" } };
147
+ expect((0, normalize_1.getDiscriminator)(schema)).toEqual({
148
+ propertyName: "type",
149
+ });
150
+ });
151
+ it("finds discriminator in oneOf branches", () => {
152
+ const schema = {
153
+ oneOf: [{ discriminator: { propertyName: "kind" } }, { type: "string" }],
154
+ };
155
+ expect((0, normalize_1.getDiscriminator)(schema)).toEqual({
156
+ propertyName: "kind",
157
+ });
158
+ });
159
+ it("finds discriminator in anyOf branches", () => {
160
+ const schema = {
161
+ anyOf: [
162
+ { type: "string" },
163
+ { discriminator: { propertyName: "variant" } },
164
+ ],
165
+ };
166
+ expect((0, normalize_1.getDiscriminator)(schema)).toEqual({
167
+ propertyName: "variant",
168
+ });
169
+ });
170
+ it("finds discriminator in allOf branches", () => {
171
+ const schema = {
172
+ allOf: [
173
+ { properties: { id: { type: "string" } } },
174
+ { discriminator: { propertyName: "type" } },
175
+ ],
176
+ };
177
+ expect((0, normalize_1.getDiscriminator)(schema)).toEqual({
178
+ propertyName: "type",
179
+ });
180
+ });
181
+ it("returns undefined when no discriminator exists", () => {
182
+ const schema = {
183
+ type: "object",
184
+ properties: { name: { type: "string" } },
185
+ };
186
+ expect((0, normalize_1.getDiscriminator)(schema)).toBeUndefined();
187
+ });
188
+ it("returns undefined for null/primitive input", () => {
189
+ expect((0, normalize_1.getDiscriminator)(null)).toBeUndefined();
190
+ expect((0, normalize_1.getDiscriminator)("string")).toBeUndefined();
191
+ });
192
+ it("caches results for same object reference", () => {
193
+ const inner = { discriminator: { propertyName: "type" } };
194
+ const schema = { oneOf: [inner] };
195
+ const first = (0, normalize_1.getDiscriminator)(schema);
196
+ const second = (0, normalize_1.getDiscriminator)(schema);
197
+ expect(first).toBe(second);
198
+ });
199
+ it("handles cycles without infinite recursion", () => {
200
+ const a = { properties: {} };
201
+ const b = { properties: { a } };
202
+ a.properties.b = b;
203
+ expect(() => (0, normalize_1.getDiscriminator)(a)).not.toThrow();
204
+ expect((0, normalize_1.getDiscriminator)(a)).toBeUndefined();
205
+ });
206
+ });
207
+ describe("findPropertyDeep", () => {
208
+ it("returns property at top level", () => {
209
+ const schema = { properties: { foo: { type: "string" } } };
210
+ expect((0, normalize_1.findPropertyDeep)(schema, "foo")).toEqual({
211
+ type: "string",
212
+ });
213
+ });
214
+ it("finds property in oneOf branches", () => {
215
+ const schema = {
216
+ oneOf: [
217
+ { properties: { a: { type: "string" } } },
218
+ { properties: { b: { type: "number" } } },
219
+ ],
220
+ };
221
+ expect((0, normalize_1.findPropertyDeep)(schema, "a")).toEqual({
222
+ type: "string",
223
+ });
224
+ expect((0, normalize_1.findPropertyDeep)(schema, "b")).toEqual({
225
+ type: "number",
226
+ });
227
+ });
228
+ it("finds property in anyOf branches", () => {
229
+ const schema = {
230
+ anyOf: [{ properties: { x: { type: "integer" } } }],
231
+ };
232
+ expect((0, normalize_1.findPropertyDeep)(schema, "x")).toEqual({
233
+ type: "integer",
234
+ });
235
+ });
236
+ it("finds property in allOf branches", () => {
237
+ const schema = {
238
+ allOf: [{ properties: { y: { type: "boolean" } } }],
239
+ };
240
+ expect((0, normalize_1.findPropertyDeep)(schema, "y")).toEqual({
241
+ type: "boolean",
242
+ });
243
+ });
244
+ it("returns undefined when property not found", () => {
245
+ const schema = { properties: { foo: { type: "string" } } };
246
+ expect((0, normalize_1.findPropertyDeep)(schema, "bar")).toBeUndefined();
247
+ });
248
+ it("returns undefined for null/primitive input", () => {
249
+ expect((0, normalize_1.findPropertyDeep)(null, "foo")).toBeUndefined();
250
+ expect((0, normalize_1.findPropertyDeep)("string", "foo")).toBeUndefined();
251
+ });
252
+ it("handles cycles without infinite recursion", () => {
253
+ const a = { properties: {} };
254
+ a.oneOf = [a];
255
+ expect(() => (0, normalize_1.findPropertyDeep)(a, "foo")).not.toThrow();
256
+ expect((0, normalize_1.findPropertyDeep)(a, "foo")).toBeUndefined();
257
+ });
258
+ });
259
+ describe("isCircularMarker", () => {
260
+ it("returns true for circular marker strings", () => {
261
+ expect((0, normalize_1.isCircularMarker)("circular(Title)")).toBe(true);
262
+ expect((0, normalize_1.isCircularMarker)("circular()")).toBe(true);
263
+ });
264
+ it("returns false for other values", () => {
265
+ expect((0, normalize_1.isCircularMarker)("normal string")).toBe(false);
266
+ expect((0, normalize_1.isCircularMarker)(null)).toBe(false);
267
+ expect((0, normalize_1.isCircularMarker)(undefined)).toBe(false);
268
+ expect((0, normalize_1.isCircularMarker)({})).toBe(false);
269
+ expect((0, normalize_1.isCircularMarker)(42)).toBe(false);
270
+ });
271
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "docusaurus-theme-openapi-docs",
3
3
  "description": "OpenAPI theme for Docusaurus.",
4
- "version": "0.0.0-1314",
4
+ "version": "0.0.0-1332",
5
5
  "license": "MIT",
6
6
  "keywords": [
7
7
  "openapi",
@@ -38,7 +38,7 @@
38
38
  "@types/postman-collection": "^3.5.11",
39
39
  "@types/react-modal": "^3.16.3",
40
40
  "concurrently": "^10.0.3",
41
- "docusaurus-plugin-openapi-docs": "0.0.0-1314",
41
+ "docusaurus-plugin-openapi-docs": "0.0.0-1332",
42
42
  "docusaurus-plugin-sass": "^0.2.6",
43
43
  "eslint-plugin-prettier": "^5.5.1"
44
44
  },
@@ -82,5 +82,5 @@
82
82
  "engines": {
83
83
  "node": ">=14"
84
84
  },
85
- "gitHead": "c8bf336e01d040d09ab74df031b680dce9e482fc"
85
+ "gitHead": "42a942eaf3b45f41329f21e1bf0ac9dde70cbd1a"
86
86
  }
@@ -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,6 +14,14 @@ 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";
17
25
  import {
18
26
  SchemaDepthProvider,
19
27
  useSchemaDepth,
@@ -22,192 +30,12 @@ import {
22
30
  import SchemaItem from "@theme/SchemaItem";
23
31
  import SchemaTabs from "@theme/SchemaTabs";
24
32
  import TabItem from "@theme/TabItem";
25
- // eslint-disable-next-line import/no-extraneous-dependencies
26
- import { merge } from "allof-merge";
27
33
  import clsx from "clsx";
28
34
  import isEmpty from "lodash/isEmpty";
29
35
 
30
36
  import { getQualifierMessage, getSchemaName } from "../../markdown/schema";
31
37
  import type { SchemaObject } from "../../types.d";
32
38
 
33
- // eslint-disable-next-line import/no-extraneous-dependencies
34
- // const jsonSchemaMergeAllOf = require("json-schema-merge-allof");
35
-
36
- /**
37
- * Strip `additionalProperties: false` from sibling allOf members so the
38
- * strict-AND semantics of `allof-merge` don't collapse the result to an
39
- * unsatisfiable empty schema.
40
- *
41
- * Per JSON Schema, two allOf members that each set `additionalProperties: false`
42
- * with disjoint `properties` sets define an unsatisfiable schema (no value can
43
- * satisfy both — each member rejects the other's properties). `allof-merge` is
44
- * technically correct to drop all properties in that case, but it leaves the
45
- * rendered schema blank.
46
- *
47
- * NSwag and Swashbuckle emit this pattern by default whenever a model uses
48
- * inheritance/composition, so it's the dominant style for .NET-generated specs.
49
- * Redoc, Swagger UI, and Stoplight all union the properties and ignore the
50
- * conflicting flag — the approach this helper emulates by stripping the flag
51
- * before delegating to `allof-merge`. The flag is render-irrelevant anyway:
52
- * `additionalProperties: false` is treated identically to `undefined` by the
53
- * AdditionalProperties component below (line ~641).
54
- *
55
- * Strips from every allOf member whenever the parent has ≥2 members. The
56
- * collapse triggers symmetrically (both siblings strict) or asymmetrically
57
- * (one strict member rejects another's properties as "additional"), so the
58
- * presence of multiple members is the right gate. Single-member allOf is left
59
- * alone — it can't conflict with anything.
60
- *
61
- * See https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issues/1119
62
- * Mirrored in plugin: docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts
63
- */
64
- const stripConflictingAdditionalProps = (node: any): any => {
65
- if (Array.isArray(node)) return node.map(stripConflictingAdditionalProps);
66
- if (!node || typeof node !== "object") return node;
67
-
68
- let working: any = node;
69
- if (Array.isArray(node.allOf) && node.allOf.length > 1) {
70
- const hasStrictMember = node.allOf.some(
71
- (m: any) => m && m.additionalProperties === false
72
- );
73
- if (hasStrictMember) {
74
- working = {
75
- ...node,
76
- allOf: node.allOf.map((m: any) => {
77
- if (m && m.additionalProperties === false) {
78
- const { additionalProperties: _drop, ...rest } = m;
79
- return rest;
80
- }
81
- return m;
82
- }),
83
- };
84
- }
85
- }
86
-
87
- const result: any = {};
88
- for (const [k, v] of Object.entries(working)) {
89
- result[k] = stripConflictingAdditionalProps(v);
90
- }
91
- return result;
92
- };
93
-
94
- const mergeAllOf = (allOf: any) => {
95
- const onMergeError = (msg: string) => {
96
- console.warn(msg);
97
- };
98
-
99
- const mergedSchemas = merge(stripConflictingAdditionalProps(allOf), {
100
- onMergeError,
101
- });
102
-
103
- return mergedSchemas ?? {};
104
- };
105
-
106
- /**
107
- * Fold sibling fields into each `oneOf`/`anyOf` branch via allOf-merge, so each
108
- * branch is self-contained. Mirrors Redoc's `SchemaModel.initOneOf` behavior.
109
- * Without this, when an `allOf` override redefines a nested property with
110
- * `oneOf`, the merged schema ends up with both `properties` and `oneOf` as
111
- * siblings — and the renderer prints the shared properties twice.
112
- *
113
- * See https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issues/1218
114
- */
115
- const foldSiblingsIntoBranches = (schema: any): any => {
116
- const branchKey = schema?.oneOf
117
- ? "oneOf"
118
- : schema?.anyOf
119
- ? "anyOf"
120
- : undefined;
121
- if (!branchKey) return schema;
122
-
123
- const branches = schema[branchKey];
124
- if (!Array.isArray(branches) || branches.length === 0) return schema;
125
-
126
- const siblings = { ...schema };
127
- delete siblings[branchKey];
128
- if (Object.keys(siblings).length === 0) return schema;
129
-
130
- const folded = branches.map((branch: any) =>
131
- mergeAllOf({ allOf: [siblings, branch] })
132
- );
133
-
134
- return { [branchKey]: folded };
135
- };
136
-
137
- /**
138
- * Recursively searches for a property in a schema, including nested
139
- * oneOf, anyOf, and allOf structures. This is needed for discriminators
140
- * where the property definition may be in a nested schema.
141
- */
142
- const findProperty = (
143
- schema: SchemaObject,
144
- propertyName: string
145
- ): SchemaObject | undefined => {
146
- // Check direct properties first
147
- if (schema.properties?.[propertyName]) {
148
- return schema.properties[propertyName];
149
- }
150
-
151
- // Search in oneOf schemas
152
- if (schema.oneOf) {
153
- for (const subschema of schema.oneOf) {
154
- const found = findProperty(subschema as SchemaObject, propertyName);
155
- if (found) return found;
156
- }
157
- }
158
-
159
- // Search in anyOf schemas
160
- if (schema.anyOf) {
161
- for (const subschema of schema.anyOf) {
162
- const found = findProperty(subschema as SchemaObject, propertyName);
163
- if (found) return found;
164
- }
165
- }
166
-
167
- // Search in allOf schemas
168
- if (schema.allOf) {
169
- for (const subschema of schema.allOf) {
170
- const found = findProperty(subschema as SchemaObject, propertyName);
171
- if (found) return found;
172
- }
173
- }
174
-
175
- return undefined;
176
- };
177
-
178
- /**
179
- * Recursively searches for a discriminator in a schema, including nested
180
- * oneOf, anyOf, and allOf structures.
181
- */
182
- const findDiscriminator = (schema: SchemaObject): any | undefined => {
183
- if (schema.discriminator) {
184
- return schema.discriminator;
185
- }
186
-
187
- if (schema.oneOf) {
188
- for (const subschema of schema.oneOf) {
189
- const found = findDiscriminator(subschema as SchemaObject);
190
- if (found) return found;
191
- }
192
- }
193
-
194
- if (schema.anyOf) {
195
- for (const subschema of schema.anyOf) {
196
- const found = findDiscriminator(subschema as SchemaObject);
197
- if (found) return found;
198
- }
199
- }
200
-
201
- if (schema.allOf) {
202
- for (const subschema of schema.allOf) {
203
- const found = findDiscriminator(subschema as SchemaObject);
204
- if (found) return found;
205
- }
206
- }
207
-
208
- return undefined;
209
- };
210
-
211
39
  interface MarkdownProps {
212
40
  text: string | undefined;
213
41
  }
@@ -388,6 +216,11 @@ const AnyOneOf: React.FC<SchemaProps> = ({
388
216
  label={label}
389
217
  value={`${uniqueId}-${index}-item`}
390
218
  >
219
+ {anyOneSchema.description && (
220
+ <div style={{ marginLeft: "1rem" }}>
221
+ <MarkdownWrapper text={anyOneSchema.description} />
222
+ </div>
223
+ )}
391
224
  {/* Handle primitive types directly */}
392
225
  {(isPrimitive(anyOneSchema) || anyOneSchema.const) && (
393
226
  <SchemaItem
@@ -571,6 +404,13 @@ const PropertyDiscriminator: React.FC<SchemaEdgeProps> = ({
571
404
  label={key}
572
405
  value={`${index}-item-discriminator`}
573
406
  >
407
+ {discriminator.mapping[key]?.description && (
408
+ <div style={{ marginLeft: "1rem" }}>
409
+ <MarkdownWrapper
410
+ text={discriminator.mapping[key].description}
411
+ />
412
+ </div>
413
+ )}
574
414
  <SchemaNode
575
415
  schema={discriminator.mapping[key]}
576
416
  schemaType={schemaType}
@@ -616,9 +456,11 @@ const DiscriminatorNode: React.FC<DiscriminatorNodeProps> = ({
616
456
  let discriminatedSchemas: any = {};
617
457
  let inferredMapping: any = {};
618
458
 
619
- // 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.
620
462
  const discriminatorProperty =
621
- findProperty(schema, discriminator.propertyName) ?? {};
463
+ findPropertyDeep(schema, discriminator.propertyName) ?? {};
622
464
 
623
465
  if (schema.allOf) {
624
466
  const mergedSchemas = mergeAllOf(schema) as SchemaObject;
@@ -699,6 +541,20 @@ const AdditionalProperties: React.FC<SchemaProps> = ({
699
541
 
700
542
  if (!additionalProperties) return null;
701
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
+
702
558
  // Handle free-form objects
703
559
  if (additionalProperties === true || isEmpty(additionalProperties)) {
704
560
  return (
@@ -812,6 +668,23 @@ const Items: React.FC<{
812
668
  schemaType: "request" | "response";
813
669
  schemaPath?: string;
814
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
+
815
688
  // Process schema.items to handle allOf merging
816
689
  let itemsSchema = schema.items;
817
690
  if (schema.items?.allOf) {
@@ -932,6 +805,20 @@ const SchemaEdge: React.FC<SchemaEdgeProps> = ({
932
805
  return null;
933
806
  }
934
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
+
935
822
  const schemaName = getSchemaName(schema);
936
823
 
937
824
  if (discriminator && discriminator.propertyName === name) {
@@ -1018,6 +905,20 @@ const SchemaEdge: React.FC<SchemaEdgeProps> = ({
1018
905
  );
1019
906
  }
1020
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
+
1021
922
  if (schema.allOf) {
1022
923
  // handle circular properties
1023
924
  if (
@@ -1176,10 +1077,12 @@ function renderChildren(
1176
1077
  }
1177
1078
 
1178
1079
  const SchemaNode: React.FC<SchemaProps> = ({
1179
- schema,
1080
+ schema: rawSchema,
1180
1081
  schemaType,
1181
1082
  schemaPath,
1182
1083
  }) => {
1084
+ const schema = useMemo(() => normalizeSchema(rawSchema), [rawSchema]);
1085
+
1183
1086
  if (
1184
1087
  (schemaType === "request" && schema.readOnly) ||
1185
1088
  (schemaType === "response" && schema.writeOnly)
@@ -1188,10 +1091,11 @@ const SchemaNode: React.FC<SchemaProps> = ({
1188
1091
  }
1189
1092
 
1190
1093
  // Resolve discriminator recursively so nested oneOf/anyOf/allOf compositions
1191
- // 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).
1192
1096
  let workingSchema = schema;
1193
1097
  const resolvedDiscriminator =
1194
- schema.discriminator ?? findDiscriminator(schema);
1098
+ schema.discriminator ?? getDiscriminator(schema);
1195
1099
  if (schema.allOf && !schema.discriminator && resolvedDiscriminator) {
1196
1100
  workingSchema = mergeAllOf(schema) as SchemaObject;
1197
1101
  }