From 4c814bb84b39718f0cba5cfed5d5b70ac56f0f07 Mon Sep 17 00:00:00 2001 From: AayushSaini101 Date: Wed, 8 Jul 2026 05:00:27 +0530 Subject: [PATCH 1/3] fix: filter out $ref keys in bindings() to prevent undefined parser output Fixes #877 When bindings objects still contain JSON reference keys, treat them as references rather than protocol names so Binding models are not built from invalid data. Co-authored-by: Cursor --- .changeset/fix-bindings-ref-filter.md | 5 +++++ packages/parser/src/models/v2/components.ts | 4 +++- packages/parser/src/models/v2/mixins.ts | 4 +++- packages/parser/src/models/v3/components.ts | 4 +++- packages/parser/src/models/v3/mixins.ts | 4 +++- packages/parser/test/models/v2/mixins.spec.ts | 7 +++++++ packages/parser/test/models/v3/mixins.spec.ts | 17 +++++++++++++++++ 7 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 .changeset/fix-bindings-ref-filter.md create mode 100644 packages/parser/test/models/v3/mixins.spec.ts diff --git a/.changeset/fix-bindings-ref-filter.md b/.changeset/fix-bindings-ref-filter.md new file mode 100644 index 000000000..42e1edd6a --- /dev/null +++ b/.changeset/fix-bindings-ref-filter.md @@ -0,0 +1,5 @@ +--- +"@asyncapi/parser": patch +--- + +fix: filter out $ref keys in bindings() to prevent undefined parser output diff --git a/packages/parser/src/models/v2/components.ts b/packages/parser/src/models/v2/components.ts index 168cedf4d..1fb335c2a 100644 --- a/packages/parser/src/models/v2/components.ts +++ b/packages/parser/src/models/v2/components.ts @@ -133,7 +133,9 @@ export class Components extends BaseModel implements Compon const asyncapi = this.meta('asyncapi'); const pointer = `components/${itemsName}/${name}`; bindings[name] = new Bindings( - Object.entries(bindingsData).map(([protocol, binding]) => + Object.entries(bindingsData) + .filter(([key]) => !key.startsWith('$')) + .map(([protocol, binding]) => this.createModel(Binding, binding, { protocol, pointer: `${pointer}/${protocol}` }) ), { originalData: bindingsData as any, asyncapi, pointer } diff --git a/packages/parser/src/models/v2/mixins.ts b/packages/parser/src/models/v2/mixins.ts index 37ebc7927..c350be029 100644 --- a/packages/parser/src/models/v2/mixins.ts +++ b/packages/parser/src/models/v2/mixins.ts @@ -21,7 +21,9 @@ import type { v2 } from '../../spec-types'; export function bindings(model: BaseModel<{ bindings?: Record }>): BindingsInterface { const bindings = model.json('bindings') || {}; return new Bindings( - Object.entries(bindings || {}).map(([protocol, binding]) => + Object.entries(bindings || {}) + .filter(([key]) => !key.startsWith('$')) + .map(([protocol, binding]) => createModel(Binding, binding, { protocol, pointer: model.jsonPath(`bindings/${protocol}`) }, model) ), { originalData: bindings, asyncapi: model.meta('asyncapi'), pointer: model.jsonPath('bindings') } diff --git a/packages/parser/src/models/v3/components.ts b/packages/parser/src/models/v3/components.ts index 9931b0b70..7c837d9ae 100644 --- a/packages/parser/src/models/v3/components.ts +++ b/packages/parser/src/models/v3/components.ts @@ -157,7 +157,9 @@ export class Components extends BaseModel implements Compon const asyncapi = this.meta('asyncapi'); const pointer = `components/${itemsName}/${name}`; bindings[name] = new Bindings( - Object.entries(bindingsData).map(([protocol, binding]) => + Object.entries(bindingsData) + .filter(([key]) => !key.startsWith('$')) + .map(([protocol, binding]) => this.createModel(Binding, binding, { protocol, pointer: `${pointer}/${protocol}` }) ), { originalData: bindingsData as any, asyncapi, pointer } diff --git a/packages/parser/src/models/v3/mixins.ts b/packages/parser/src/models/v3/mixins.ts index 9191b4008..e67bc43de 100644 --- a/packages/parser/src/models/v3/mixins.ts +++ b/packages/parser/src/models/v3/mixins.ts @@ -83,7 +83,9 @@ export abstract class CoreModel): BindingsInterface { const bindings = model.json('bindings') || {}; return new Bindings( - Object.entries(bindings || {}).map(([protocol, binding]) => + Object.entries(bindings || {}) + .filter(([key]) => !key.startsWith('$')) + .map(([protocol, binding]) => createModel(Binding, binding, { protocol, pointer: model.jsonPath(`bindings/${protocol}`) }, model) ), { originalData: bindings as Record, asyncapi: model.meta('asyncapi'), pointer: model.jsonPath('bindings') } diff --git a/packages/parser/test/models/v2/mixins.spec.ts b/packages/parser/test/models/v2/mixins.spec.ts index e5c4d0e70..18daf8b83 100644 --- a/packages/parser/test/models/v2/mixins.spec.ts +++ b/packages/parser/test/models/v2/mixins.spec.ts @@ -24,6 +24,13 @@ describe('mixins', function() { expect(bindings(d3)).toBeInstanceOf(BindingsV2); expect(bindings(d3).length).toEqual(0); }); + + it('should ignore JSON reference keys in bindings', function() { + const doc = { bindings: { $ref: '#/components/messageBindings/myBindings', amqp: { test: 'test1' } } }; + const model = new Model(doc); + expect(bindings(model).length).toEqual(1); + expect(bindings(model).all()[0].protocol()).toEqual('amqp'); + }); }); describe('hasDescription', function() { diff --git a/packages/parser/test/models/v3/mixins.spec.ts b/packages/parser/test/models/v3/mixins.spec.ts new file mode 100644 index 000000000..ae30299bf --- /dev/null +++ b/packages/parser/test/models/v3/mixins.spec.ts @@ -0,0 +1,17 @@ +import { BaseModel } from '../../../src/models/base'; +import { bindings } from '../../../src/models/v3/mixins'; +import { BindingsV3 } from '../../../src/models/v3'; + +describe('mixins', function() { + describe('bindings', function() { + class Model extends BaseModel {} + + it('should ignore JSON reference keys in bindings', function() { + const doc = { bindings: { $ref: '#/components/messageBindings/myBindings', kafka: { bindingVersion: '0.4.0' } } }; + const model = new Model(doc); + expect(bindings(model)).toBeInstanceOf(BindingsV3); + expect(bindings(model).length).toEqual(1); + expect(bindings(model).all()[0].protocol()).toEqual('kafka'); + }); + }); +}); From d52541f290632e233e6eccd43865ce0ff6e21354 Mon Sep 17 00:00:00 2001 From: AayushSaini101 Date: Wed, 8 Jul 2026 08:57:09 +0530 Subject: [PATCH 2/3] Fix lint issue --- packages/parser/src/models/v2/components.ts | 4 ++-- packages/parser/src/models/v2/mixins.ts | 4 ++-- packages/parser/src/models/v3/components.ts | 4 ++-- packages/parser/src/models/v3/mixins.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/parser/src/models/v2/components.ts b/packages/parser/src/models/v2/components.ts index 1fb335c2a..89064f521 100644 --- a/packages/parser/src/models/v2/components.ts +++ b/packages/parser/src/models/v2/components.ts @@ -136,8 +136,8 @@ export class Components extends BaseModel implements Compon Object.entries(bindingsData) .filter(([key]) => !key.startsWith('$')) .map(([protocol, binding]) => - this.createModel(Binding, binding, { protocol, pointer: `${pointer}/${protocol}` }) - ), + this.createModel(Binding, binding, { protocol, pointer: `${pointer}/${protocol}` }) + ), { originalData: bindingsData as any, asyncapi, pointer } ); return bindings; diff --git a/packages/parser/src/models/v2/mixins.ts b/packages/parser/src/models/v2/mixins.ts index c350be029..68b33cb1d 100644 --- a/packages/parser/src/models/v2/mixins.ts +++ b/packages/parser/src/models/v2/mixins.ts @@ -24,8 +24,8 @@ export function bindings(model: BaseModel<{ bindings?: Record }>): Object.entries(bindings || {}) .filter(([key]) => !key.startsWith('$')) .map(([protocol, binding]) => - createModel(Binding, binding, { protocol, pointer: model.jsonPath(`bindings/${protocol}`) }, model) - ), + createModel(Binding, binding, { protocol, pointer: model.jsonPath(`bindings/${protocol}`) }, model) + ), { originalData: bindings, asyncapi: model.meta('asyncapi'), pointer: model.jsonPath('bindings') } ); } diff --git a/packages/parser/src/models/v3/components.ts b/packages/parser/src/models/v3/components.ts index 7c837d9ae..12471e2f4 100644 --- a/packages/parser/src/models/v3/components.ts +++ b/packages/parser/src/models/v3/components.ts @@ -160,8 +160,8 @@ export class Components extends BaseModel implements Compon Object.entries(bindingsData) .filter(([key]) => !key.startsWith('$')) .map(([protocol, binding]) => - this.createModel(Binding, binding, { protocol, pointer: `${pointer}/${protocol}` }) - ), + this.createModel(Binding, binding, { protocol, pointer: `${pointer}/${protocol}` }) + ), { originalData: bindingsData as any, asyncapi, pointer } ); return bindings; diff --git a/packages/parser/src/models/v3/mixins.ts b/packages/parser/src/models/v3/mixins.ts index e67bc43de..b1b081adf 100644 --- a/packages/parser/src/models/v3/mixins.ts +++ b/packages/parser/src/models/v3/mixins.ts @@ -86,8 +86,8 @@ export function bindings(model: BaseModel<{ bindings?: BindingsObject }>): Bindi Object.entries(bindings || {}) .filter(([key]) => !key.startsWith('$')) .map(([protocol, binding]) => - createModel(Binding, binding, { protocol, pointer: model.jsonPath(`bindings/${protocol}`) }, model) - ), + createModel(Binding, binding, { protocol, pointer: model.jsonPath(`bindings/${protocol}`) }, model) + ), { originalData: bindings as Record, asyncapi: model.meta('asyncapi'), pointer: model.jsonPath('bindings') } ); } From 83f17ce876169f51d63b06723948e2712d96bfaf Mon Sep 17 00:00:00 2001 From: AayushSaini101 Date: Sun, 12 Jul 2026 15:04:20 +0530 Subject: [PATCH 3/3] fix: skip unknown schema formats during parse instead of throwing When a schema format is not registered, return the raw schema and continue parsing so documents with bindings and other metadata remain accessible without requiring AvroSchemaParser registration. Co-authored-by: Cursor --- packages/parser/src/schema-parser/index.ts | 4 +- .../custom-operations/parse-schema-v2.spec.ts | 13 +++- .../custom-operations/parse-schema-v3.spec.ts | 67 ++++++++++++++++++- 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/packages/parser/src/schema-parser/index.ts b/packages/parser/src/schema-parser/index.ts index a40f944c0..ee7d08931 100644 --- a/packages/parser/src/schema-parser/index.ts +++ b/packages/parser/src/schema-parser/index.ts @@ -45,10 +45,10 @@ export async function validateSchema(parser: Parser, input: ValidateSchemaInput) return schemaParser.validate(input); } -export async function parseSchema(parser: Parser, input: ParseSchemaInput) { +export async function parseSchema(parser: Parser, input: ParseSchemaInput): Promise { const schemaParser = parser.parserRegistry.get(input.schemaFormat); if (schemaParser === undefined) { - throw new Error('Unknown schema format'); + return input.data as AsyncAPISchema; } return schemaParser.parse(input); } diff --git a/packages/parser/test/custom-operations/parse-schema-v2.spec.ts b/packages/parser/test/custom-operations/parse-schema-v2.spec.ts index 8504bb64d..00175c93c 100644 --- a/packages/parser/test/custom-operations/parse-schema-v2.spec.ts +++ b/packages/parser/test/custom-operations/parse-schema-v2.spec.ts @@ -101,7 +101,7 @@ describe('custom operations for v2 - parse schemas', function() { expect((document?.json()?.channels?.channel?.publish?.message as v2.MessageObject)?.payload === (document?.json().components?.messages?.message as v2.MessageObject)?.payload).toEqual(true); }); - it('should parse invalid schema format', async function() { + it('should skip parsing unknown schema format and still return document', async function() { const documentRaw = { asyncapi: '2.0.0', info: { @@ -122,9 +122,16 @@ describe('custom operations for v2 - parse schemas', function() { } } }; - const { document, diagnostics } = await parser.parse(documentRaw); + const { document, diagnostics } = await parser.parse(documentRaw, { + validateOptions: { allowedSeverity: { error: true } }, + }); - expect(document).toBeUndefined(); + expect(document).toBeInstanceOf(AsyncAPIDocumentV2); expect(diagnostics.length > 0).toEqual(true); + expect(diagnostics.some(d => d.message?.includes('Unknown schema format'))).toEqual(true); + expect((document?.json()?.channels?.channel?.publish?.message as v2.MessageObject)?.payload).toEqual({ + type: 'object', + 'x-parser-schema-id': '', + }); }); }); diff --git a/packages/parser/test/custom-operations/parse-schema-v3.spec.ts b/packages/parser/test/custom-operations/parse-schema-v3.spec.ts index c8c3ea87f..becf4a1f0 100644 --- a/packages/parser/test/custom-operations/parse-schema-v3.spec.ts +++ b/packages/parser/test/custom-operations/parse-schema-v3.spec.ts @@ -105,7 +105,7 @@ describe('custom operations for v3 - parse schemas', function() { expect(((document?.json()?.channels?.channel as v3.ChannelObject).messages?.message as v3.MessageObject)?.payload).toEqual({ type: 'object', 'x-parser-schema-id': '' }); }); - it('should parse invalid schema format', async function() { + it('should skip parsing unknown schema format and still return document', async function() { const documentRaw = { asyncapi: '3.0.0', info: { @@ -130,7 +130,70 @@ describe('custom operations for v3 - parse schemas', function() { }; const { document, diagnostics } = await parser.parse(documentRaw); - expect(document).toBeUndefined(); + 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': '', + }); + }); + + it('should parse avro schema format without registering AvroSchemaParser', async function() { + const documentRaw = { + asyncapi: '3.1.0', + info: { + title: 'Adeo AsyncAPI Case Study', + version: '1.0.0', + }, + channels: { + costingRequestChannel: { + address: 'adeo-{env}-case-study-COSTING-REQUEST-{version}', + bindings: { + kafka: { + replicas: 3, + partitions: 3, + }, + }, + messages: { + CostingRequest: { + payload: { + schemaFormat: 'application/vnd.apache.avro;version=1.9.0', + schema: { + type: 'record', + name: 'CostingRequest', + fields: [], + }, + }, + }, + }, + }, + }, + operations: { + receiveACostingRequest: { + action: 'receive', + channel: { + $ref: '#/channels/costingRequestChannel', + }, + bindings: { + kafka: { + groupId: { + type: 'string', + }, + }, + }, + }, + }, + }; + const { document, diagnostics } = await parser.parse(documentRaw); + + expect(document).toBeInstanceOf(AsyncAPIDocumentV3); + expect(filterLastVersionDiagnostics(diagnostics).length === 0).toEqual(true); + expect(document!.allChannels().get('costingRequestChannel')?.bindings().get('kafka')?.json()).toEqual({ + replicas: 3, + partitions: 3, + }); + expect(document!.allOperations().get('receiveACostingRequest')?.bindings().get('kafka')?.json()?.groupId).toEqual({ + type: 'string', + }); }); });