jsonschema2md icon indicating copy to clipboard operation
jsonschema2md copied to clipboard

Recursive reference causes problem

Open savasp opened this issue 4 years ago • 1 comments

Hi all,

I encountered an issue when trying to generate documentation from a set of schemas that contain a circular reference.

{
    "$id": "https://example.org/foo",
    "$schema": "http://json-schema.org/draft-07/schema#",

    "type": "object",
    "properties": {
        "foo": {
            "$ref": "#/definitions/bar"
        }
    },

    "definitions": {
        "bar": {
            "oneOf": [
                {
                   "type": "string"
                },
                {
                    "type": "array",
                    "items": {
                        "$ref": "#/definitions/bar"
                    }
                }
            ]        
        }
    }
}

I get the following exception...

(node:53742) UnhandledPromiseRejectionWarning: RangeError: Maximum call stack size exceeded
    at formatRaw (internal/util/inspect.js:1014:12)
    at formatValue (internal/util/inspect.js:793:10)
    at inspect (internal/util/inspect.js:326:10)
    at formatWithOptionsInternal (internal/util/inspect.js:1994:40)
    at formatWithOptions (internal/util/inspect.js:1878:10)
    at Object.value (internal/console/constructor.js:306:14)
    at Object.log (internal/console/constructor.js:341:61)
    at reducer (/usr/local/lib/node_modules/@adobe/jsonschema2md/lib/traverseSchema.js:17:11)
    at Array.reduce (<anonymous>)
    at reducer (/usr/local/lib/node_modules/@adobe/jsonschema2md/lib/traverseSchema.js:29:39)
(Use `node --trace-warnings ...` to show where the warning was created)
(node:53742) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:53742) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

The schema seems to be valid. I can avoid the issue by rewriting the schemas as...

{
    "$id": "https://example.org/foo",
    "$schema": "http://json-schema.org/draft-07/schema#",
    "type": "object",
    "properties": {
        "foo": {
            "$ref": "#/definitions/bar"
        }
    },
    "definitions": {
        "bar": {
            "$id": "https://example.org/bar",
            "oneOf": [
                {
                    "type": "string"
                },
                {
                    "type": "array",
                    "items": {
                        "$ref": "https://example.org/bar"
                    }
                }
            ]
        }
    }
}

It seems that the error surfaces only with relative references.

savasp avatar Oct 09 '20 06:10 savasp

When traversing a schema, it keeps track of a Set of "seen" items, but the "get" from the proxy handler returns a new Proxy object for every subschema, whether it has been retrieved previously or not, so the recursively retrieved subschema is a new Proxy and is not in the "seen" Set.

On the schemaProxy side, if you create a registry of previously proxied subschemas and reuse them, then the "seen" mechanism seems to work as it should. (With this patch applied, your example does not throw an error)

diff --git a/lib/schemaProxy.js b/lib/schemaProxy.js
index 2010be2..338f070 100644
--- a/lib/schemaProxy.js
+++ b/lib/schemaProxy.js
@@ -30,7 +30,7 @@ function loadExamples(file, num = 1) {
 }
 
 const handler = ({
-  root = '', filename = '.', schemas, parent, slugger,
+  root = '', filename = '.', schemas, subschemas, parent, slugger,
 }) => {
   const meta = {};
 
@@ -157,19 +157,29 @@ const handler = ({
         }
 
         // console.log('making new proxy from', target, prop, 'receiver', receiver[symbols.id]);
-        const subschema = new Proxy(retval, handler({
-          root: `${root}/${prop}`,
-          parent: receiver,
-          filename,
-          schemas,
-          slugger,
-        }));
+        let subschema;
+        if (subschemas.has(retval)) {
+          subschema = subschemas.get(retval);
+        }
+        else {
+          subschema = new Proxy(retval, handler({
+            root: `${root}/${prop}`,
+            parent: receiver,
+            filename,
+            schemas,
+            subschemas,
+            slugger,
+          }));
+
+          subschemas.set(retval, subschema);
+        }
 
         if (subschema[keyword`$id`]) {
           // stow away the schema for lookup
           // eslint-disable-next-line no-param-reassign
           schemas.known[subschema[keyword`$id`]] = subschema;
         }
+
         return subschema;
       }
       return retval;
@@ -187,12 +197,13 @@ module.exports.loader = () => {
     known: {},
     files: {},
   };
+  const subschemas = new Map();
 
   const slugger = ghslugger();
 
   return (schema, filename) => {
     // console.log('loading', filename);
-    const proxied = new Proxy(schema, handler({ filename, schemas, slugger }));
+    const proxied = new Proxy(schema, handler({ filename, schemas, subschemas, slugger }));
     schemas.loaded.push(proxied);
     if (proxied[keyword`$id`]) {
       // stow away the schema for lookup

bmaranville avatar Mar 24 '21 18:03 bmaranville

:tada: This issue has been resolved in version 7.1.2 :tada:

The release is available on:

Your semantic-release bot :package::rocket:

trieloff avatar Oct 06 '22 15:10 trieloff