From a25fdedea920f4bf6c3dd41e848451b30859b61c Mon Sep 17 00:00:00 2001
From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com>
Date: Wed, 26 Mar 2025 15:16:03 +0100
Subject: [PATCH] Replace $ref with $reference in json-decycle (#3049)
---
.changeset/warm-ducks-check.md | 5 ++
bun.lock | 3 -
packages/react-openapi/package.json | 1 -
packages/react-openapi/src/OpenAPISchema.tsx | 27 +++-----
.../react-openapi/src/OpenAPISchemaServer.tsx | 2 +-
packages/react-openapi/src/decycle.ts | 68 +++++++++++++++++++
6 files changed, 82 insertions(+), 24 deletions(-)
create mode 100644 .changeset/warm-ducks-check.md
create mode 100644 packages/react-openapi/src/decycle.ts
diff --git a/.changeset/warm-ducks-check.md b/.changeset/warm-ducks-check.md
new file mode 100644
index 000000000..fe20bed7f
--- /dev/null
+++ b/.changeset/warm-ducks-check.md
@@ -0,0 +1,5 @@
+---
+'@gitbook/react-openapi': patch
+---
+
+Replace $ref with $reference in json-decycle
diff --git a/bun.lock b/bun.lock
index 0e59b1dbd..60e9df304 100644
--- a/bun.lock
+++ b/bun.lock
@@ -241,7 +241,6 @@
"@scalar/oas-utils": "^0.2.120",
"clsx": "^2.1.1",
"flatted": "^3.2.9",
- "json-decycle": "^4.0.0",
"json-xml-parse": "^1.3.0",
"react-aria": "^3.37.0",
"react-aria-components": "^1.6.0",
@@ -2070,8 +2069,6 @@
"json-buffer": ["json-buffer@3.0.0", "", {}, "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ=="],
- "json-decycle": ["json-decycle@4.0.0", "", {}, "sha512-3GFL/vWazCbMu1kw+NdIfAHh6Ugq5pxkKcSUnK1f/Fw1nDtt1i+BiBfRJs0iPEKscYAz4k4+osvgjY95hmuJXQ=="],
-
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
diff --git a/packages/react-openapi/package.json b/packages/react-openapi/package.json
index d5f0ddf37..15ede1ffd 100644
--- a/packages/react-openapi/package.json
+++ b/packages/react-openapi/package.json
@@ -16,7 +16,6 @@
"@scalar/oas-utils": "^0.2.120",
"clsx": "^2.1.1",
"flatted": "^3.2.9",
- "json-decycle": "^4.0.0",
"json-xml-parse": "^1.3.0",
"react-aria-components": "^1.6.0",
"react-aria": "^3.37.0",
diff --git a/packages/react-openapi/src/OpenAPISchema.tsx b/packages/react-openapi/src/OpenAPISchema.tsx
index 65ee7c546..a012a1ec5 100644
--- a/packages/react-openapi/src/OpenAPISchema.tsx
+++ b/packages/react-openapi/src/OpenAPISchema.tsx
@@ -6,10 +6,10 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import { useId } from 'react';
import clsx from 'clsx';
-import { retrocycle } from 'json-decycle';
import { Markdown } from './Markdown';
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
import { OpenAPISchemaName } from './OpenAPISchemaName';
+import { retrocycle } from './decycle';
import type { OpenAPIClientContext } from './types';
import { checkIsReference, resolveDescription, resolveFirstExample } from './utils';
@@ -122,7 +122,7 @@ export function OpenAPISchemaPropertiesFromServer(props: {
return (
);
@@ -172,7 +172,12 @@ export function OpenAPIRootSchemaFromServer(props: {
schema: string;
context: OpenAPIClientContext;
}) {
- return ;
+ return (
+
+ );
}
/**
@@ -460,19 +465,3 @@ function getDisclosureLabel(schema: OpenAPIV3.SchemaObject): string {
return schema.title || 'child attributes';
}
-
-/**
- * Safely parse a JSON string using retrocycle.
- * If parsing fails, it falls back to standard JSON.parse.
- */
-function safeJSONParse(jsonString: string) {
- try {
- return JSON.parse(jsonString, retrocycle());
- } catch {
- try {
- return JSON.parse(jsonString);
- } catch (fallbackError) {
- throw new Error(`Failed to parse JSON string: ${fallbackError}`);
- }
- }
-}
diff --git a/packages/react-openapi/src/OpenAPISchemaServer.tsx b/packages/react-openapi/src/OpenAPISchemaServer.tsx
index 877d378d6..d2396c705 100644
--- a/packages/react-openapi/src/OpenAPISchemaServer.tsx
+++ b/packages/react-openapi/src/OpenAPISchemaServer.tsx
@@ -1,10 +1,10 @@
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
-import { decycle } from 'json-decycle';
import {
OpenAPIRootSchemaFromServer,
OpenAPISchemaPropertiesFromServer,
type OpenAPISchemaPropertyEntry,
} from './OpenAPISchema';
+import { decycle } from './decycle';
import type { OpenAPIClientContext } from './types';
export function OpenAPISchemaProperties(props: {
diff --git a/packages/react-openapi/src/decycle.ts b/packages/react-openapi/src/decycle.ts
new file mode 100644
index 000000000..bb2da6a8f
--- /dev/null
+++ b/packages/react-openapi/src/decycle.ts
@@ -0,0 +1,68 @@
+// Forked from: https://github.com/YChebotaev/json-decycle/blob/master/src/index.ts
+// Replaced `$ref` with `$reference` to avoid conflicts with OpenAPI
+
+const isObject = (value: any): value is object =>
+ typeof value === 'object' &&
+ value != null &&
+ !(value instanceof Boolean) &&
+ !(value instanceof Date) &&
+ !(value instanceof Number) &&
+ !(value instanceof RegExp) &&
+ !(value instanceof String);
+
+const toPointer = (parts: string[]) =>
+ `#${parts.map((part) => String(part).replace(/~/g, '~0').replace(/\//g, '~1')).join('/')}`;
+
+export const decycle = () => {
+ const paths = new WeakMap();
+
+ return function replacer(this: any, key: string | symbol, value: any) {
+ if (key !== '$reference' && isObject(value)) {
+ const seen = paths.has(value);
+
+ if (seen) {
+ return { $reference: toPointer(paths.get(value)) };
+ }
+
+ paths.set(value, [...(paths.get(this) ?? []), key]);
+ }
+
+ return value;
+ };
+};
+
+export function retrocycle() {
+ const parents = new WeakMap();
+ const keys = new WeakMap();
+ const refs = new Set();
+
+ function dereference(this: { [k: string]: any }, ref: { $reference: string }) {
+ const parts = ref.$reference.slice(1).split('/');
+ let key: any;
+ let value = this;
+
+ for (let i = 0; i < parts.length; i++) {
+ key = parts[i]?.replace(/~1/g, '/').replace(/~0/g, '~');
+ value = value[key];
+ }
+
+ const parent = parents.get(ref);
+ parent[keys.get(ref)] = value;
+ }
+
+ return function reviver(this: object, key: string | symbol, value: any) {
+ if (key === '$reference') {
+ refs.add(this);
+ } else if (isObject(value)) {
+ const isRoot = key === '' && Object.keys(this).length === 1;
+ if (isRoot) {
+ refs.forEach(dereference as any, this);
+ } else {
+ parents.set(value, this);
+ keys.set(value, key);
+ }
+ }
+
+ return value;
+ };
+}