Repository: asyncapi/parser-js
Pull request #864 (2023-10-03 15:02:43)

fix: schema parsing for v3 
**Description**
This PR fixes a few things:

- Fixes schemas are not parsed when having different schema formats
- Fixes v3 message trait with headers was using incorrect types as they can now be multiformat schemas.

**Related issue(s)**
Fixes https://github.com/asyncapi/parser-js/issues/859

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=asyncapi_parser-js&pullRequest=864)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=asyncapi_parser-js&pullRequest=864) No Coverage information  
[![No Duplication information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo-16px.png 'No Duplication information')](https://sonarcloud.io/component_measures?id=asyncapi_parser-js&pullRequest=864&metric=duplicated_lines_density&view=list) No Duplication information


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=asyncapi_parser-js&pullRequest=864)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=asyncapi_parser-js&pullRequest=864) No Coverage information  
[![No Duplication information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo-16px.png 'No Duplication information')](https://sonarcloud.io/component_measures?id=asyncapi_parser-js&pullRequest=864&metric=duplicated_lines_density&view=list) No Duplication information


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=asyncapi_parser-js&pullRequest=864)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=asyncapi_parser-js&pullRequest=864) No Coverage information  
[![No Duplication information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo-16px.png 'No Duplication information')](https://sonarcloud.io/component_measures?id=asyncapi_parser-js&pullRequest=864&metric=duplicated_lines_density&view=list) No Duplication information


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=asyncapi_parser-js&pullRequest=864)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=864&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=asyncapi_parser-js&pullRequest=864) No Coverage information  
[![No Duplication information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo-16px.png 'No Duplication information')](https://sonarcloud.io/component_measures?id=asyncapi_parser-js&pullRequest=864&metric=duplicated_lines_density&view=list) No Duplication information


jonaslagoni: /rtm

Patch:
diff --git a/src/custom-operations/anonymous-naming.ts b/src/custom-operations/anonymous-naming.ts
index d29b9e9..88522f3 100644
--- a/src/custom-operations/anonymous-naming.ts
+++ b/src/custom-operations/anonymous-naming.ts
@@ -1,6 +1,6 @@
 import { xParserMessageName, xParserSchemaId } from '../constants';
 import { traverseAsyncApiDocument } from '../iterator';
-import { setExtension } from '../utils';
+import { setExtension, setExtensionOnJson } from '../utils';
 
 import type { 
   AsyncAPIDocumentInterface,
@@ -59,12 +59,15 @@ function assignUidToComponentSchemas(document: AsyncAPIDocumentInterface) {
     setExtension(xParserSchemaId, schema.id(), schema);
   });
 }
-  
+
 function assignUidToAnonymousSchemas(doc: AsyncAPIDocumentInterface) {
   let anonymousSchemaCounter = 0;
   function callback(schema: SchemaInterface) {
+    const json = schema.json() as any;
+    const isMultiFormatSchema = json.schema !== undefined;
+    const underlyingSchema = isMultiFormatSchema ? json.schema : json;
     if (!schema.id()) {
-      setExtension(xParserSchemaId, `<anonymous-schema-${++anonymousSchemaCounter}>`, schema);
+      setExtensionOnJson(xParserSchemaId, `<anonymous-schema-${++anonymousSchemaCounter}>`, underlyingSchema);
     }
   }
   traverseAsyncApiDocument(doc, callback);
diff --git a/src/custom-operations/index.ts b/src/custom-operations/index.ts
index e360848..2875466 100644
--- a/src/custom-operations/index.ts
+++ b/src/custom-operations/index.ts
@@ -1,8 +1,7 @@
 import { applyTraitsV2, applyTraitsV3 } from './apply-traits';
-import { checkCircularRefs } from './check-circular-refs';
-import { parseSchemasV2 } from './parse-schema';
-import { anonymousNaming } from './anonymous-naming';
 import { resolveCircularRefs } from './resolve-circular-refs';
+import { parseSchemasV2, parseSchemasV3 } from './parse-schema';
+import { anonymousNaming } from './anonymous-naming';
 
 import type { RulesetFunctionContext } from '@stoplight/spectral-core';
 import type { Parser } from '../parser';
@@ -10,6 +9,7 @@ import type { ParseOptions } from '../parse';
 import type { AsyncAPIDocumentInterface } from '../models';
 import type { DetailedAsyncAPI } from '../types';
 import type { v2, v3 } from '../spec-types';
+import { checkCircularRefs } from './check-circular-refs';
 
 export async function customOperations(parser: Parser, document: AsyncAPIDocumentInterface, detailed: DetailedAsyncAPI, inventory: RulesetFunctionContext['documentInventory'], options: ParseOptions): Promise<void> {
   switch (detailed.semver.major) {
@@ -28,7 +28,7 @@ async function operationsV2(parser: Parser, document: AsyncAPIDocumentInterface,
     await parseSchemasV2(parser, detailed);
   }
 
-  // anonymous naming and resolving circular refrences should be done after custom schemas parsing
+  // anonymous naming and resolving circular references should be done after custom schemas parsing
   if (inventory) {
     resolveCircularRefs(document, inventory);
   }
@@ -41,9 +41,12 @@ async function operationsV3(parser: Parser, document: AsyncAPIDocumentInterface,
   if (options.applyTraits) {
     applyTraitsV3(detailed.parsed as v3.AsyncAPIObject);
   }
-  // TODO: Support schema parsing in v3
-  // if (options.parseSchemas) {
-  //   await parseSchemasV2(parser, detailed);
-  // }
+  if (options.parseSchemas) {
+    await parseSchemasV3(parser, detailed);
+  }
+  // anonymous naming and resolving circular references should be done after custom schemas parsing
+  if (inventory) {
+    resolveCircularRefs(document, inventory);
+  }
   anonymousNaming(document);
 }
diff --git a/src/custom-operations/parse-schema.ts b/src/custom-operations/parse-schema.ts
index 6221af6..d33c017 100644
--- a/src/custom-operations/parse-schema.ts
+++ b/src/custom-operations/parse-schema.ts
@@ -22,6 +22,24 @@ const customSchemasPathsV2 = [
   '$.components.messages.*',
 ];
 
+const customSchemasPathsV3 = [
+  // channels
+  '$.channels.*.messages.*.payload',
+  '$.channels.*.messages.*.headers',
+  '$.components.channels.*.messages.*.payload',
+  '$.components.channels.*.messages.*.headers',
+  // operations
+  '$.operations.*.messages.*.payload',
+  '$.operations.*.messages.*.headers',
+  '$.components.operations.*.messages.*.payload',
+  '$.components.operations.*.messages.*.headers',
+  // messages
+  '$.components.messages.*.payload',
+  '$.components.messages.*.headers.*',
+  // schemas 
+  '$.components.schemas.*',
+];
+
 export async function parseSchemasV2(parser: Parser, detailed: DetailedAsyncAPI) {
   const defaultSchemaFormat = getDefaultSchemaFormat(detailed.semver.version);
   const parseItems: Array<ToParseItem> = [];
@@ -65,6 +83,68 @@ export async function parseSchemasV2(parser: Parser, detailed: DetailedAsyncAPI)
   return Promise.all(parseItems.map(item => parseSchemaV2(parser, item)));
 }
 
+export async function parseSchemasV3(parser: Parser, detailed: DetailedAsyncAPI) {
+  const defaultSchemaFormat = getDefaultSchemaFormat(detailed.semver.version);
+  const parseItems: Array<ToParseItem> = [];
+
+  const visited: Set<unknown> = new Set();
+  customSchemasPathsV3.forEach(path => {
+    JSONPath({
+      path,
+      json: detailed.parsed,
+      resultType: 'all',
+      callback(result) {
+        const value = result.value;
+        if (visited.has(value)) {
+          return;
+        }
+        visited.add(value);
+
+        const schema = value.schema;
+        if (!schema) {
+          return;
+        }
+
+        let schemaFormat = value.schemaFormat;
+        if (!schemaFormat) {
+          return;
+        }
+        schemaFormat = getSchemaFormat(value.schemaFormat, detailed.semver.version);
+
+        parseItems.push({
+          input: {
+            asyncapi: detailed,
+            data: schema,
+            meta: {
+              message: value,
+            },
+            path: [...splitPath(result.path), 'schema'],
+            schemaFormat,
+            defaultSchemaFormat,
+          },
+          value,
+        });
+      },
+    });
+  });
+
+  return Promise.all(parseItems.map(item => parseSchemaV3(parser, item)));
+}
+
+async function parseSchemaV3(parser: Parser, item: ToParseItem) {
+  const originalData = item.input.data;
+  const parsedData = await parseSchema(parser, item.input);
+  if (item.value?.schema !== undefined) {
+    item.value.schema = parsedData;
+  } else {
+    item.value = parsedData;
+  }
+  // save original payload only when data is different (returned by custom parsers)
+  if (originalData !== parsedData) {
+    item.value[xParserOriginalPayload] = originalData;
+  }
+}
+
 async function parseSchemaV2(parser: Parser, item: ToParseItem) {
   const originalData = item.input.data;
   const parsedData = item.value.payload = await parseSchema(parser, item.input);
diff --git a/src/models/v3/schema.ts b/src/models/v3/schema.ts
index 763c91a..40e3d53 100644
--- a/src/models/v3/schema.ts
+++ b/src/models/v3/schema.ts
@@ -47,7 +47,7 @@ export class Schema extends BaseModel<v3.MultiFormatSchemaObject, { id?: string,
 
   $schema(): string {
     if (typeof this._schemaObject === 'boolean') return 'http://json-schema.org/draft-07/schema#';
-    return this._schemaObject.$schema || 'http://json-schema.org/draft-07/schema#';
+    return this._schemaObject.$schema ?? 'http://json-schema.org/draft-07/schema#';
   }
 
   additionalItems(): boolean | SchemaInterface {
diff --git a/src/spec-types/v3.ts b/src/spec-types/v3.ts
index e0fc849..cacd212 100644
--- a/src/spec-types/v3.ts
+++ b/src/spec-types/v3.ts
@@ -181,9 +181,8 @@ export interface MessageObject extends MessageTraitObject, SpecificationExtensio
 
 export interface MessageTraitObject extends SpecificationExtensions {
   messageId?: string;
-  headers?: SchemaObject;
+  headers?: MultiFormatSchemaObject;
   correlationId?: CorrelationIDObject | ReferenceObject;
-  schemaFormat?: string;
   contentType?: string;
   name?: string;
   title?: string;
@@ -403,7 +402,8 @@ export interface OAuthFlowObjectAuthorizationCode extends OAuthFlowObjectBase, S
 
 export type SchemaObject = AsyncAPISchemaObject | ReferenceObject;
 export type AsyncAPISchemaObject = AsyncAPISchemaDefinition | boolean;
-export type MultiFormatSchemaObject = AsyncAPISchemaObject | { schema: AsyncAPISchemaObject, schemaFormat: string | undefined }
+export type MultiFormatObject = { schema: AsyncAPISchemaObject, schemaFormat: string | undefined }
+export type MultiFormatSchemaObject = AsyncAPISchemaObject | MultiFormatObject
 
 export interface AsyncAPISchemaDefinition extends SpecificationExtensions {
   $id?: string;
diff --git a/src/utils.ts b/src/utils.ts
index 0690d02..fdb0237 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -51,9 +51,13 @@ export function hasHintDiagnostic(diagnostics: ISpectralDiagnostic[]): boolean {
 
 export function setExtension(id: string, value: unknown, model: BaseModel): void {
   const modelValue = model.json();
-  if (typeof modelValue === 'object' && modelValue) {
+  setExtensionOnJson(id, value, modelValue);
+}
+
+export function setExtensionOnJson(id: string, value: unknown, model: any): void {
+  if (typeof model === 'object' && model) {
     id = id.startsWith('x-') ? id : `x-${id}`;
-    modelValue[String(id)] = value;
+    model[String(id)] = value;
   }
 }
 


Test patch:
diff --git a/test/custom-operations/parse-schema.spec.ts b/test/custom-operations/parse-schema-v2.spec.ts
similarity index 98%
rename from test/custom-operations/parse-schema.spec.ts
rename to test/custom-operations/parse-schema-v2.spec.ts
index dfd71f2..8504bb6 100644
--- a/test/custom-operations/parse-schema.spec.ts
+++ b/test/custom-operations/parse-schema-v2.spec.ts
@@ -3,7 +3,7 @@ import { Parser } from '../../src/parser';
 
 import type { v2 } from '../../src/spec-types';
 
-describe('custom operations - parse schemas', function() {
+describe('custom operations for v2 - parse schemas', function() {
   const parser = new Parser();
 
   it('should parse valid schema format', async function() {
diff --git a/test/custom-operations/parse-schema-v3.spec.ts b/test/custom-operations/parse-schema-v3.spec.ts
new file mode 100644
index 0000000..fab592b
--- /dev/null
+++ b/test/custom-operations/parse-schema-v3.spec.ts
@@ -0,0 +1,135 @@
+import { AsyncAPIDocumentV3 } from '../../src/models';
+import { Parser } from '../../src/parser';
+
+import type { v3 } from '../../src/spec-types';
+
+describe('custom operations for v3 - parse schemas', function() {
+  const parser = new Parser();
+
+  it('should parse valid schema format and preserve reference', async function() {
+    const documentRaw = {
+      asyncapi: '3.0.0',
+      info: {
+        title: 'Valid AsyncApi document',
+        version: '1.0'
+      },
+      channels: {
+        channel: {
+          $ref: '#/components/channels/channel'
+        }
+      },
+      operations: {
+        operation: {
+          action: 'receive',
+          channel: {
+            $ref: '#/channels/channel'
+          },
+          messages: [
+            {
+              $ref: '#/components/messages/message'
+            }
+          ]
+        }
+      },
+      components: {
+        channels: {
+          channel: {
+            address: 'channel',
+            messages: {
+              message: {
+                $ref: '#/components/messages/message'
+              }
+            }
+          }
+        },
+        messages: {
+          message: {
+            headers: {
+              $ref: '#/components/schemas/schema'
+            },
+            payload: {
+              $ref: '#/components/schemas/schema'
+            }
+          }
+        },
+        schemas: {
+          schema: {
+            schemaFormat: 'application/vnd.aai.asyncapi;version=2.0.0',
+            schema: {
+              type: 'object'
+            }
+          }
+        }
+      }
+    };
+    const { document, diagnostics } = await parser.parse(documentRaw);
+
+    expect(document).toBeInstanceOf(AsyncAPIDocumentV3);
+    expect(diagnostics.length === 0).toEqual(true);
+
+    expect(((document?.json()?.channels?.channel as v3.ChannelObject).messages?.message as v3.MessageObject)?.payload?.schema).toEqual({ type: 'object', 'x-parser-schema-id': '<anonymous-schema-1>' });
+    expect(((((document?.json() as any).operations?.operation as v3.OperationObject).channel as v3.ChannelObject)?.messages?.message as v3.MessageObject)?.payload?.schema).toEqual({ type: 'object', 'x-parser-schema-id': '<anonymous-schema-1>' });
+    expect((((document?.json() as any).operations?.operation as v3.OperationObject).messages?.[0] as v3.MessageObject)?.payload?.schema).toEqual({ type: 'object', 'x-parser-schema-id': '<anonymous-schema-1>' });
+    expect(((document?.json()?.components?.channels?.channel as v3.ChannelObject).messages?.message as v3.MessageObject)?.payload?.schema).toEqual({ type: 'object', 'x-parser-schema-id': '<anonymous-schema-1>' });
+    expect((document?.json()?.components?.messages?.message as v3.MessageObject)?.payload?.schema).toEqual({ type: 'object', 'x-parser-schema-id': '<anonymous-schema-1>' });
+    expect((((document?.json() as any).components?.messages?.message as v3.MessageObject)?.headers as v3.MultiFormatObject).schema).toEqual({ type: 'object', 'x-parser-schema-id': '<anonymous-schema-1>' });
+    expect((document?.json() as any).components?.schemas?.schema?.schema).toEqual({ type: 'object', 'x-parser-schema-id': '<anonymous-schema-1>' });
+  });
+
+  it('should parse valid default schema format', async function() {
+    const documentRaw = {
+      asyncapi: '3.0.0',
+      info: {
+        title: 'Valid AsyncApi document',
+        version: '1.0'
+      },
+      channels: {
+        channel: {
+          address: 'channel',
+          messages: {
+            message: {
+              payload: {
+                type: 'object'
+              }
+            }
+          }
+        }
+      }
+    };
+    const { document, diagnostics } = await parser.parse(documentRaw);
+    
+    expect(document).toBeInstanceOf(AsyncAPIDocumentV3);
+    expect(diagnostics.length === 0).toEqual(true);
+
+    expect(((document?.json()?.channels?.channel as v3.ChannelObject).messages?.message as v3.MessageObject)?.payload).toEqual({ type: 'object', 'x-parser-schema-id': '<anonymous-schema-1>' });
+  });
+
+  it('should parse invalid schema format', async function() {
+    const documentRaw = {
+      asyncapi: '3.0.0',
+      info: {
+        title: 'Valid AsyncApi document',
+        version: '1.0'
+      },
+      channels: {
+        channel: {
+          address: 'channel',
+          messages: {
+            message: {
+              payload: {
+                schemaFormat: 'not existing',
+                schema: {
+                  type: 'object'
+                }
+              }
+            }
+          }
+        }
+      }
+    };
+    const { document, diagnostics } = await parser.parse(documentRaw);
+    
+    expect(document).toBeUndefined();
+    expect(diagnostics.length > 0).toEqual(true);
+  });
+});