Repository: asyncapi/parser-js (ts)

Issue:
Support all edge cases with circular references
With switching to the Spectral, we found that we do not support all cases with circular references (or, more precisely, one particular case). Spectral has its own `ref-resolver` that does not resolve circular references like `json-schema-ref-parser`, but only leaves the `$ref` field intact at the point when such a reference is encountered. I described that problem in one of [issue](https://github.com/asyncapi/parser-js/pull/591#discussion_r964497593):

> Circular references you can only have with Schema Objects, so `'/components/channels/test'.includes('/channels/test')` is not a good example.
>
> You can create circular reference only when you point to the schema which is already used in parent path(s), so schema like:
>
> ```yaml
> type: object
> properties:
>   deepProperty:
>     type: object
>     properties:
>       circular:
>         $ref: '#/properties/deepProperty'
> ```
>
> is circular in `properties/deepProperty/properties/circular` path. That case is handled by first `if` in mentioned function
>
> Another case is related to Spectral and how it resolves circular references. We can have two two schemas, main one and external > (from file):
>
> ```yaml
> type: object
> properties:
>   fileProperty:
>     $ref: './fromfile.yaml'
>     
> # fromfile.yaml
> type: object
> properties:
>   deepProperty:
>     type: object
>     properties:
>       circular:
>         $ref: '#/properties/deepProperty'
> ```
>
> but in this case the circular reference after dereferencing has `$ref` field still with `'#/properties/deepProperty'` value, so it means that if we will check path we will have `'#/properties/deepProperty'` from file pov, not from main file pov, like `'#/properties/fileProperty/properties/deepProperty'` and due to this fact we need to also check (by second if) the subpath. I hope it's clear now. Above logic is dictated by Spectral "problems".

it's resolved in our parser side, but one case is very hard to resolve like:

```yaml
type: object
properties:
  fileProperty:
    $ref: './fromfile#/properties/circular1'
        
# fromfile.yaml
type: object
properties:
  circular1:
    type: object
    properties:
      circular:
        $ref: '#/properties/circular2'
  circular2:
    type: object
    properties:
      circular:
        $ref: '#/properties/circular1'
```

and we don't have any additional data (metadata?) to resolve such a reference. We should switch to the `json-schema-ref-resolver` in Spectral or fix that problem in Spectral's `ref-resolver` (but that resolver is deprecated). We can also wait for https://github.com/stoplightio/spectral/issues/1054

Pull request description:
fix: handle complex circular reference cases
<!--   Thank you for your contribution. Before you submit the pull request:
1. Follow contributing guidelines and make sure PR title follows Conventional Commits specification -> https://github.com/asyncapi/generator/blob/master/CONTRIBUTING.md
2. Test your changes and attach their results to the pull request.
3. Update the relevant documentation.
-->

**Description**

Support edge circular reference cases:
- resolve circular refs to appropriate resolved object in JSON tree.
- update `SchemaV2` and `SchemaOld` models.
- remove unnecessary functions.
- update unit test.

**Related issue(s)**
Resolves #599 

Patch:
diff --git a/src/custom-operations/index.ts b/src/custom-operations/index.ts
index ee7b331..521bbc5 100644
--- a/src/custom-operations/index.ts
+++ b/src/custom-operations/index.ts
@@ -1,21 +1,22 @@
 import { applyTraitsV2 } from './apply-traits';
-import { checkCircularRefs } from './check-circular-refs';
+import { resolveCircularRefs } from './resolve-circular-refs';
 import { parseSchemasV2 } from './parse-schema';
 import { anonymousNaming } from './anonymous-naming';
 
+import type { RulesetFunctionContext } from '@stoplight/spectral-core';
 import type { Parser } from '../parser';
 import type { ParseOptions } from '../parse';
 import type { AsyncAPIDocumentInterface } from '../models';
 import type { DetailedAsyncAPI } from '../types';
 
-export async function customOperations(parser: Parser, document: AsyncAPIDocumentInterface, detailed: DetailedAsyncAPI, options: ParseOptions): Promise<void> {
+export async function customOperations(parser: Parser, document: AsyncAPIDocumentInterface, detailed: DetailedAsyncAPI, inventory: RulesetFunctionContext['documentInventory'], options: ParseOptions): Promise<void> {
   switch (detailed.semver.major) {
-  case 2: return operationsV2(parser, document, detailed, options);
+  case 2: return operationsV2(parser, document, detailed, inventory, options);
   // case 3: return operationsV3(parser, document, detailed, options);
   }
 }
 
-async function operationsV2(parser: Parser, document: AsyncAPIDocumentInterface, detailed: DetailedAsyncAPI, options: ParseOptions): Promise<void> {
+async function operationsV2(parser: Parser, document: AsyncAPIDocumentInterface, detailed: DetailedAsyncAPI, inventory: RulesetFunctionContext['documentInventory'], options: ParseOptions): Promise<void> {
   if (options.applyTraits) {
     applyTraitsV2(detailed.parsed);
   }
@@ -23,8 +24,10 @@ async function operationsV2(parser: Parser, document: AsyncAPIDocumentInterface,
     await parseSchemasV2(parser, detailed);
   }
 
-  // anonymous naming and checking circular refrences should be done after custom schemas parsing
-  checkCircularRefs(document);
+  // anonymous naming and resolving circular refrences should be done after custom schemas parsing
+  if (inventory) {
+    resolveCircularRefs(document, inventory);
+  }
   anonymousNaming(document);
 }
 
diff --git a/src/custom-operations/resolve-circular-refs.ts b/src/custom-operations/resolve-circular-refs.ts
new file mode 100644
index 0000000..388c2d8
--- /dev/null
+++ b/src/custom-operations/resolve-circular-refs.ts
@@ -0,0 +1,67 @@
+import { setExtension, toJSONPathArray, retrieveDeepData, findSubArrayIndex } from '../utils';
+import { xParserCircular } from '../constants';
+
+import type { RulesetFunctionContext } from '@stoplight/spectral-core';
+import type { AsyncAPIDocumentInterface } from '../models';
+import type { AsyncAPIObject } from '../types';
+
+interface Context { 
+  document: AsyncAPIObject;
+  hasCircular: boolean;
+  inventory: RulesetFunctionContext['documentInventory'];
+  visited: Set<any>;
+}
+
+export function resolveCircularRefs(document: AsyncAPIDocumentInterface, inventory: RulesetFunctionContext['documentInventory']) {
+  const documentJson = document.json();
+  const ctx: Context = { document: documentJson, hasCircular: false, inventory, visited: new Set() };
+  traverse(documentJson, [], null, '', ctx);
+  if (ctx.hasCircular) {
+    setExtension(xParserCircular, true, document);
+  }
+}
+
+function traverse(data: any, path: Array<string | number>, parent: any, property: string | number, ctx: Context) {
+  if (typeof data !== 'object' || !data || ctx.visited.has(data)) {
+    return;
+  }
+
+  ctx.visited.add(data);
+  if (Array.isArray(data)) {
+    data.forEach((item, idx) => traverse(item, [...path, idx], data, idx, ctx));
+  }
+  if ('$ref' in data) {
+    ctx.hasCircular = true;
+    const resolvedRef = retrieveCircularRef(data, path, ctx);
+    if (resolvedRef) {
+      parent[property] = resolvedRef;
+    }
+  } else {
+    for (const p in data) {
+      traverse(data[p], [...path, p], data, p, ctx);
+    }
+  }
+  ctx.visited.delete(data);
+}
+
+function retrieveCircularRef(data: { $ref: string }, path: Array<string | number>, ctx: Context): any {
+  const $refPath = toJSONPathArray(data.$ref);
+  const item = ctx.inventory.findAssociatedItemForPath(path, true);
+
+  // root document case
+  if (item === null) {
+    return retrieveDeepData(ctx.document, $refPath);
+  }
+  
+  // referenced document case
+  if (item) {
+    const subArrayIndex = findSubArrayIndex(path, $refPath);
+    let dataPath: Array<string | number> | undefined;
+    if (subArrayIndex === -1) { // create subarray based on location of the assiociated document - use item.path
+      dataPath = [...path.slice(0, path.length - item.path.length), ...$refPath];
+    } else { // create subarray based on $refPath
+      dataPath = path.slice(0, subArrayIndex + $refPath.length);
+    }
+    return retrieveDeepData(ctx.document, dataPath);
+  }
+}
diff --git a/src/models/v2/schema.ts b/src/models/v2/schema.ts
index ade229a..20ae875 100644
--- a/src/models/v2/schema.ts
+++ b/src/models/v2/schema.ts
@@ -2,9 +2,7 @@ import { BaseModel } from '../base';
 
 import { xParserSchemaId } from '../../constants';
 import { extensions, hasExternalDocs, externalDocs } from './mixins';
-import { retrievePossibleRef, hasRef } from '../../utils';
 
-import type { ModelMetadata } from '../base';
 import type { ExtensionsInterface } from '../extensions';
 import type { ExternalDocumentationInterface } from '../external-docs';
 import type { SchemaInterface } from '../schema';
@@ -12,14 +10,6 @@ import type { SchemaInterface } from '../schema';
 import type { v2 } from '../../spec-types';
 
 export class Schema extends BaseModel<v2.AsyncAPISchemaObject, { id?: string, parent?: Schema }> implements SchemaInterface {
-  constructor(
-    _json: v2.AsyncAPISchemaObject,
-    _meta: ModelMetadata & { id?: string, parent?: Schema } = {} as any,
-  ) {
-    _json = retrievePossibleRef(_json, _meta.pointer, _meta.asyncapi?.parsed);
-    super(_json, _meta);
-  }
-
   id(): string {
     return this.$id() || this._meta.id || this.json(xParserSchemaId as any) as string;
   }
@@ -164,7 +154,6 @@ export class Schema extends BaseModel<v2.AsyncAPISchemaObject, { id?: string, pa
   }
 
   isCircular(): boolean {
-    if (hasRef(this._json)) return true;
     let parent = this._meta.parent;
     while (parent) {
       if (parent._json === this._json) return true;
diff --git a/src/old-api/base.ts b/src/old-api/base.ts
index bbb0261..494d0f9 100644
--- a/src/old-api/base.ts
+++ b/src/old-api/base.ts
@@ -1,8 +1,12 @@
 export abstract class Base<J extends any = any, M extends Record<string, any> = Record<string, any>> {
   constructor(
-    protected readonly _json: J, // TODO: Add error here like in original codebase
+    protected readonly _json: J,
     protected readonly _meta: M = {} as M,
-  ) {}
+  ) {
+    if (_json === undefined || _json === null) {
+      throw new Error('Invalid JSON to instantiate the Base object.');
+    }
+  }
 
   json<T = J>(): T;
   json<K extends keyof J>(key: K): J[K];
diff --git a/src/old-api/schema.ts b/src/old-api/schema.ts
index d99125b..ef0ee92 100644
--- a/src/old-api/schema.ts
+++ b/src/old-api/schema.ts
@@ -1,6 +1,5 @@
 import { SpecificationExtensionsModel, createMapOfType, getMapValue, description, hasDescription, hasExternalDocs, externalDocs } from './mixins';
 import { xParserCircular, xParserCircularProps } from '../constants';
-import { hasRef } from '../utils';
 
 import type { Base } from './base';
 import type { v2 } from '../spec-types';
@@ -236,7 +235,7 @@ export class Schema extends SpecificationExtensionsModel<v2.AsyncAPISchemaObject
   }
 
   isCircular() {
-    if (hasRef(this._json) || this.ext(xParserCircular)) {
+    if (this.ext(xParserCircular)) {
       return true;
     }
 
diff --git a/src/parse.ts b/src/parse.ts
index 6e57bbd..a0518a8 100644
--- a/src/parse.ts
+++ b/src/parse.ts
@@ -8,7 +8,7 @@ import { createDetailedAsyncAPI, mergePatch, setExtension, createUncaghtDiagnost
 
 import { xParserSpecParsed } from './constants';
 
-import type { Spectral, Document } from '@stoplight/spectral-core';
+import type { Spectral, Document, RulesetFunctionContext } from '@stoplight/spectral-core';
 import type { Parser } from './parser';
 import type { ResolverOptions } from './resolver';
 import type { ValidateOptions } from './validate';
@@ -54,14 +54,14 @@ export async function parse(parser: Parser, spectral: Spectral, asyncapi: Input,
     }
 
     spectralDocument = extras.document;
+    const inventory: RulesetFunctionContext['documentInventory'] = (spectralDocument as any).__documentInventory;
   
     // unfreeze the object - Spectral makes resolved document "freezed" 
     const validatedDoc = copy(validated as Record<string, any>);
-    
     const detailed = createDetailedAsyncAPI(validatedDoc, asyncapi as DetailedAsyncAPI['input'], options.source);
     const document = createAsyncAPIDocument(detailed);
     setExtension(xParserSpecParsed, true, document);
-    await customOperations(parser, document, detailed, options);
+    await customOperations(parser, document, detailed, inventory, options);
   
     return { 
       document,
diff --git a/src/spectral.ts b/src/spectral.ts
index cd5a877..6ee99fd 100644
--- a/src/spectral.ts
+++ b/src/spectral.ts
@@ -49,7 +49,10 @@ function asyncApi2IsAsyncApi(): RuleDefinition {
           input: null,
           options: null,
         },
-        (targetVal) => {
+        (targetVal, _, { document, documentInventory }) => {
+          // adding document inventory in document - we need it in custom operations to resolve all circular refs
+          (document as any).__documentInventory = documentInventory;
+
           if (!isObject(targetVal) || typeof targetVal.asyncapi !== 'string') {
             return [
               {
diff --git a/src/types.ts b/src/types.ts
index d9e7633..46a575e 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -13,7 +13,7 @@ export interface AsyncAPISemver {
 
 export interface DetailedAsyncAPI {
   source?: string;
-  input: string | MaybeAsyncAPI | AsyncAPIObject;
+  input?: string | MaybeAsyncAPI | AsyncAPIObject;
   parsed: AsyncAPIObject;
   semver: AsyncAPISemver;
 }
diff --git a/src/utils.ts b/src/utils.ts
index c6b1527..e5150b7 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -5,7 +5,7 @@ import type { ISpectralDiagnostic } from '@stoplight/spectral-core';
 import type { BaseModel } from './models';
 import type { AsyncAPISemver, AsyncAPIObject, DetailedAsyncAPI, MaybeAsyncAPI, Diagnostic } from './types';
 
-export function createDetailedAsyncAPI(parsed: AsyncAPIObject, input: string | MaybeAsyncAPI | AsyncAPIObject, source?: string): DetailedAsyncAPI {
+export function createDetailedAsyncAPI(parsed: AsyncAPIObject, input?: string | MaybeAsyncAPI | AsyncAPIObject, source?: string): DetailedAsyncAPI {
   return {
     source,
     input,
@@ -87,7 +87,7 @@ export function hasRef(value: unknown): value is { $ref: string } {
 }
 
 export function toJSONPathArray(jsonPath: string): Array<string | number> {
-  return jsonPath.split('/').map(untilde);
+  return splitPath(serializePath(jsonPath));
 }
 
 export function createUncaghtDiagnostic(err: unknown, message: string, document?: Document): Diagnostic[] {
@@ -127,22 +127,24 @@ export function untilde(str: string) {
   });
 }
 
-export function retrievePossibleRef(data: any, pathOfData: string, spec: any = {}): any {
-  if (!hasRef(data)) {
-    return data;
-  }
-
-  const refPath = serializePath(data.$ref);
-  if (pathOfData.startsWith(refPath)) { // starts by given path
-    return retrieveDeepData(spec, splitPath(refPath)) || data;
-  } else if (pathOfData.includes(refPath)) { // circular path in substring of path
-    const substringPath = pathOfData.split(refPath)[0];
-    return retrieveDeepData(spec, splitPath(`${substringPath}${refPath}`)) || data;
+export function findSubArrayIndex(arr: Array<any>, subarr: Array<any>, fromIndex = 0) {
+  let i, found, j;
+  for (i = fromIndex; i < 1 + (arr.length - subarr.length); ++i) {
+    found = true;
+    for (j = 0; j < subarr.length; ++j) {
+      if (arr[i + j] !== subarr[j]) {
+        found = false;
+        break;
+      }
+    }
+    if (found) {
+      return i;
+    }
   }
-  return data;
+  return -1;
 }
 
-function retrieveDeepData(value: Record<string, any>, path: string[]) {
+export function retrieveDeepData(value: Record<string, any>, path: Array<string | number>) {
   let index = 0;
   const length = path.length;
 
diff --git a/src/validate.ts b/src/validate.ts
index 70334b7..6eb51f9 100644
--- a/src/validate.ts
+++ b/src/validate.ts
@@ -26,7 +26,7 @@ export interface ValidateOutput {
   validated: unknown;
   diagnostics: Diagnostic[];
   extras: {
-    document: Document,
+    document: Document;
   }
 }
 
@@ -52,7 +52,7 @@ export async function validate(parser: Parser, parserSpectral: Spectral, asyncap
   
     const spectral = options.__unstable?.resolver ? createSpectral(parser, options.__unstable?.resolver) : parserSpectral;
     // eslint-disable-next-line prefer-const
-    let { resolved: validated, results } = await spectral.runWithResolved(document);
+    let { resolved: validated, results } = await spectral.runWithResolved(document, {  });
   
     if (
       (!allowedSeverity?.error && hasErrorDiagnostic(results)) ||


Test patch:
diff --git a/test/custom-operations/check-circular-refs.spec.ts b/test/custom-operations/check-circular-refs.spec.ts
deleted file mode 100644
index 89bfdb9..0000000
--- a/test/custom-operations/check-circular-refs.spec.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { Parser } from '../../src/parser';
-import { xParserCircular } from '../../src/constants';
-
-describe('custom operations - check circular references', function() {
-  const parser = new Parser();
-
-  it('should not assign x-parser-circular extension when document has not circular schemas', async function() {
-    const { document } = await parser.parse({
-      asyncapi: '2.0.0',
-      info: {
-        title: 'Valid AsyncApi document',
-        version: '1.0',
-      },
-      channels: {
-        channel: {
-          publish: {
-            operationId: 'operation',
-            message: {
-              payload: {
-                property1: {
-                  $ref: '#/channels/channel/publish/message/payload/property2'
-                },
-                property2: {
-                  type: 'string',
-                }
-              },
-            }
-          }
-        }
-      }
-    });
-
-    expect(document?.extensions().get(xParserCircular)?.value()).toEqual(undefined);
-  });
-
-  it('should assign x-parser-circular extension when document has circular schemas', async function() {
-    const { document } = await parser.parse({
-      asyncapi: '2.0.0',
-      info: {
-        title: 'Valid AsyncApi document',
-        version: '1.0',
-      },
-      channels: {
-        channel: {
-          publish: {
-            operationId: 'operation',
-            message: {
-              payload: {
-                property: {
-                  $ref: '#/channels/channel/publish/message/payload'
-                },
-              },
-            }
-          }
-        }
-      }
-    });
-
-    expect(document?.extensions().get(xParserCircular)?.value()).toEqual(true);
-  });
-});
diff --git a/test/custom-operations/resolve-circular-refs.spec.ts b/test/custom-operations/resolve-circular-refs.spec.ts
new file mode 100644
index 0000000..a1005a0
--- /dev/null
+++ b/test/custom-operations/resolve-circular-refs.spec.ts
@@ -0,0 +1,124 @@
+import { Parser } from '../../src/parser';
+import { xParserCircular } from '../../src/constants';
+
+describe('custom operations - check circular references', function() {
+  const parser = new Parser();
+
+  it('should not assign x-parser-circular extension when document has not circular schemas', async function() {
+    const { document } = await parser.parse({
+      asyncapi: '2.0.0',
+      info: {
+        title: 'Valid AsyncApi document',
+        version: '1.0',
+      },
+      channels: {
+        channel: {
+          publish: {
+            operationId: 'operation',
+            message: {
+              payload: {
+                properties: {
+                  property1: {
+                    $ref: '#/channels/channel/publish/message/payload/property2'
+                  },
+                  property2: {
+                    type: 'string',
+                  }
+                }
+              },
+            }
+          }
+        }
+      }
+    });
+
+    expect(document?.extensions().get(xParserCircular)?.value()).toEqual(undefined);
+  });
+
+  it('should assign x-parser-circular extension when document has circular schemas', async function() {
+    const { document } = await parser.parse({
+      asyncapi: '2.0.0',
+      info: {
+        title: 'Valid AsyncApi document',
+        version: '1.0',
+      },
+      channels: {
+        channel: {
+          publish: {
+            operationId: 'operation',
+            message: {
+              payload: {
+                properties: {
+                  property: {
+                    $ref: '#/channels/channel/publish/message/payload'
+                  }
+                },
+              },
+            }
+          }
+        }
+      }
+    });
+
+    const documentData = document?.json() as any;
+    expect(document?.extensions().get(xParserCircular)?.value()).toEqual(true);
+    expect(documentData.channels.channel.publish.message.payload.properties.property).not.toBeUndefined();
+    expect(documentData.channels.channel.publish.message.payload.properties.property === documentData.channels.channel.publish.message.payload).toEqual(true);
+  });
+
+  it('should assign x-parser-circular extension when document has circular schemas on external files', async function() {
+    const { document } = await parser.parse({
+      asyncapi: '2.0.0',
+      info: {
+        title: 'Valid AsyncApi document',
+        version: '1.0',
+      },
+      channels: {
+        channel: {
+          publish: {
+            operationId: 'operation',
+            message: {
+              payload: {
+                $ref: '../mocks/parse/circular-ref.yaml'
+              },
+            }
+          }
+        }
+      }
+    }, { source: __filename });
+
+    const documentData = document?.json() as any;
+    expect(document?.extensions().get(xParserCircular)?.value()).toEqual(true);
+    expect(documentData.channels.channel.publish.message.payload.properties.deepProperty.properties.circular).not.toBeUndefined();
+    expect(documentData.channels.channel.publish.message.payload.properties.deepProperty.properties.circular === documentData.channels.channel.publish.message.payload.properties.deepProperty).toEqual(true);
+  });
+
+  it('should assign x-parser-circular extension when document has circular schemas on external files (deep case)', async function() {
+    const { document, diagnostics } = await parser.parse({
+      asyncapi: '2.0.0',
+      info: {
+        title: 'Valid AsyncApi document',
+        version: '1.0',
+      },
+      channels: {
+        channel: {
+          publish: {
+            operationId: 'operation',
+            message: {
+              payload: {
+                $ref: '../mocks/parse/circular-ref-deep.yaml'
+              },
+            }
+          }
+        }
+      }
+    }, { source: __filename });
+
+    const documentData = document?.json() as any;
+    expect(document?.extensions().get(xParserCircular)?.value()).toEqual(true);
+    expect(documentData.channels.channel.publish.message.payload.properties.ExternalFile.properties.testExt).not.toBeUndefined();
+    expect(documentData.channels.channel.publish.message.payload.properties.ExternalFile.properties.testExt === documentData.channels.channel.publish.message.payload.properties.YetAnother).toEqual(true);
+    expect(documentData.channels.channel.publish.message.payload.properties.YetAnother.properties.children.items).not.toBeUndefined();
+    expect(documentData.channels.channel.publish.message.payload.properties.YetAnother.properties.children.items === documentData.channels.channel.publish.message.payload.properties.ExternalFile).toEqual(true);
+  });
+});
diff --git a/test/mocks/parse/circular-ref-deep.yaml b/test/mocks/parse/circular-ref-deep.yaml
new file mode 100644
index 0000000..b695545
--- /dev/null
+++ b/test/mocks/parse/circular-ref-deep.yaml
@@ -0,0 +1,14 @@
+type: object
+properties:
+  ExternalFile:
+    type: object
+    properties:
+      testExt:
+        $ref: '#/properties/YetAnother'
+  YetAnother:
+    type: object
+    properties:
+      children:
+        type: array
+        items:
+          $ref: '#/properties/ExternalFile'
\ No newline at end of file
diff --git a/test/old-api/asyncapi.spec.ts b/test/old-api/asyncapi.spec.ts
index 6741e57..a67d2c7 100644
--- a/test/old-api/asyncapi.spec.ts
+++ b/test/old-api/asyncapi.spec.ts
@@ -951,7 +951,7 @@ describe('AsyncAPIDocument', function() {
       const nestedSchemas = fs.readFileSync(source, 'utf8');
       const { document } = await parser.parse(nestedSchemas, { source });
       const doc = convertToOldAPI(document!);
-      const output = {asyncapi: '2.0.0',info: {title: 'Test API',version: '1.0.0'},channels: {mychannel: {publish: {message: {payload: {type: 'object',properties: {name: {type: 'string','x-parser-schema-id': '<anonymous-schema-2>'}},'x-parser-schema-id': '<anonymous-schema-1>'},'x-parser-message-name': '<anonymous-message-1>','x-parser-original-schema-format': 'application/vnd.aai.asyncapi;version=2.0.0',schemaFormat: 'application/vnd.aai.asyncapi;version=2.0.0','x-parser-original-payload': '$ref:$.channels.mychannel.publish.message.payload','x-parser-message-parsed': true}}}},components: {messages: {testMessage: {payload: {type: 'object',properties: {name: {type: 'string','x-parser-schema-id': '<anonymous-schema-3>'},deep: {type: 'object',properties: {circular: {$ref: '#/components/schemas/testSchema','x-parser-schema-id': '<anonymous-schema-5>'}},'x-parser-schema-id': '<anonymous-schema-4>'}},'x-parser-schema-id': 'testSchema'},'x-parser-message-name': 'testMessage','x-parser-original-schema-format': 'application/vnd.aai.asyncapi;version=2.0.0',schemaFormat: 'application/vnd.aai.asyncapi;version=2.0.0','x-parser-original-payload': '$ref:$.components.messages.testMessage.payload','x-parser-message-parsed': true}},schemas: {testSchema: '$ref:$.components.messages.testMessage.payload'}},'x-parser-spec-parsed': true,'x-parser-circular': true,'x-parser-spec-stringified': true};
+      const output = {asyncapi: '2.0.0',info: {title: 'Test API',version: '1.0.0'},channels: {mychannel: {publish: {message: {payload: {type: 'object',properties: {name: {type: 'string','x-parser-schema-id': '<anonymous-schema-2>'}},'x-parser-schema-id': '<anonymous-schema-1>'},'x-parser-message-name': '<anonymous-message-1>','x-parser-original-schema-format': 'application/vnd.aai.asyncapi;version=2.0.0',schemaFormat: 'application/vnd.aai.asyncapi;version=2.0.0','x-parser-original-payload': '$ref:$.channels.mychannel.publish.message.payload','x-parser-message-parsed': true}}}},components: {messages: {testMessage: {payload: {type: 'object',properties: {name: {type: 'string','x-parser-schema-id': '<anonymous-schema-3>'},deep: {type: 'object',properties: {circular: '$ref:$.components.messages.testMessage.payload'},'x-parser-schema-id': '<anonymous-schema-4>'}},'x-parser-schema-id': 'testSchema'},'x-parser-message-name': 'testMessage','x-parser-original-schema-format': 'application/vnd.aai.asyncapi;version=2.0.0',schemaFormat: 'application/vnd.aai.asyncapi;version=2.0.0','x-parser-original-payload': '$ref:$.components.messages.testMessage.payload','x-parser-message-parsed': true}},schemas: {testSchema: '$ref:$.components.messages.testMessage.payload'}},'x-parser-spec-parsed': true,'x-parser-circular': true,'x-parser-spec-stringified': true};
 
       const stringified = AsyncAPIDocument.stringify(doc);
       expect(stringified).toEqual(JSON.stringify(output));
@@ -963,6 +963,7 @@ describe('AsyncAPIDocument', function() {
       const { document } = await parser.parse(nestedSchemas, { source });
       const doc = convertToOldAPI(document!);
       const stringified = AsyncAPIDocument.stringify(doc) as string;
+
       expect(doc.json()[xParserSpecStringified]).toEqual(undefined);
       expect(JSON.parse(stringified)[xParserSpecStringified]).toEqual(true);
     });
@@ -996,7 +997,7 @@ describe('AsyncAPIDocument', function() {
     });
 
     it('should parse stringified document with circular references', async function() {
-      const circularOutput = {asyncapi: '2.0.0',info: {title: 'Test API',version: '1.0.0'},channels: {mychannel: {publish: {message: {payload: {type: 'object',properties: {name: {type: 'string','x-parser-schema-id': '<anonymous-schema-2>'}},'x-parser-schema-id': '<anonymous-schema-1>'},'x-parser-message-name': '<anonymous-message-1>','x-parser-original-schema-format': 'application/vnd.aai.asyncapi;version=2.0.0',schemaFormat: 'application/vnd.aai.asyncapi;version=2.0.0','x-parser-original-payload': '$ref:$.channels.mychannel.publish.message.payload','x-parser-message-parsed': true}}}},components: {messages: {testMessage: {payload: {type: 'object',properties: {name: {type: 'string','x-parser-schema-id': '<anonymous-schema-3>'},deep: {type: 'object',properties: {circular: {$ref: '#/components/schemas/testSchema','x-parser-schema-id': '<anonymous-schema-5>'}},'x-parser-schema-id': '<anonymous-schema-4>'}},'x-parser-schema-id': 'testSchema'},'x-parser-message-name': 'testMessage','x-parser-original-schema-format': 'application/vnd.aai.asyncapi;version=2.0.0',schemaFormat: 'application/vnd.aai.asyncapi;version=2.0.0','x-parser-original-payload': '$ref:$.components.messages.testMessage.payload','x-parser-message-parsed': true}},schemas: {testSchema: '$ref:$.components.messages.testMessage.payload'}},'x-parser-spec-parsed': true,'x-parser-circular': true,'x-parser-spec-stringified': true};
+      const circularOutput = {asyncapi: '2.0.0',info: {title: 'Test API',version: '1.0.0'},channels: {mychannel: {publish: {message: {payload: {type: 'object',properties: {name: {type: 'string','x-parser-schema-id': '<anonymous-schema-2>'}},'x-parser-schema-id': '<anonymous-schema-1>'},'x-parser-message-name': '<anonymous-message-1>','x-parser-original-schema-format': 'application/vnd.aai.asyncapi;version=2.0.0',schemaFormat: 'application/vnd.aai.asyncapi;version=2.0.0','x-parser-original-payload': '$ref:$.channels.mychannel.publish.message.payload','x-parser-message-parsed': true}}}},components: {messages: {testMessage: {payload: {type: 'object',properties: {name: {type: 'string','x-parser-schema-id': '<anonymous-schema-3>'},deep: {type: 'object',properties: {circular: '$ref:$.components.messages.testMessage.payload'},'x-parser-schema-id': '<anonymous-schema-4>'}},'x-parser-schema-id': 'testSchema'},'x-parser-message-name': 'testMessage','x-parser-original-schema-format': 'application/vnd.aai.asyncapi;version=2.0.0',schemaFormat: 'application/vnd.aai.asyncapi;version=2.0.0','x-parser-original-payload': '$ref:$.components.messages.testMessage.payload','x-parser-message-parsed': true}},schemas: {testSchema: '$ref:$.components.messages.testMessage.payload'}},'x-parser-spec-parsed': true,'x-parser-circular': true,'x-parser-spec-stringified': true};
       const result = AsyncAPIDocument.parse(circularOutput) as AsyncAPIDocument;
 
       expect(result.hasCircular()).toEqual(true);