From 1f16e17a0353de4887cacd91f87d9d97d2fde1a3 Mon Sep 17 00:00:00 2001 From: shyam-raghuwanshi Date: Sun, 22 Feb 2026 21:59:14 +0530 Subject: [PATCH 1/2] feat: add tests and migration docs for using message name for anonymous payload schemas --- docs/migrations/version-5-to-6.md | 55 ++++++ .../processors/AsyncAPIInputProcessor.spec.ts | 156 ++++++++++++++++++ 2 files changed, 211 insertions(+) diff --git a/docs/migrations/version-5-to-6.md b/docs/migrations/version-5-to-6.md index b4ac724c35..a7fe52ea19 100644 --- a/docs/migrations/version-5-to-6.md +++ b/docs/migrations/version-5-to-6.md @@ -110,6 +110,61 @@ Generated model: `AnonymousSchema_1` or `Payload` **After (v6):** Generated model: `UserSignedupPayload` (derived from channel path `/user/signedup`) +#### Named messages with anonymous payloads (fixes #1996) + +When using named messages in `components/messages` with inline (anonymous) payload schemas, the generated model now uses the message name instead of generic `AnonymousSchema_1`. + +**Before (v5):** + +```yaml +asyncapi: 2.2.0 +info: + title: Account Service + version: 1.0.0 +channels: + user/signedup: + subscribe: + message: + $ref: '#/components/messages/UserSignedUp' +components: + messages: + UserSignedUp: + payload: + type: object + properties: + displayName: + type: string + email: + type: string + format: email +``` + +Generated model: `AnonymousSchema_1` + +**After (v6):** + +Generated model: `UserSignedUpPayload` (derived from message name `UserSignedUp`) + +This also works for AsyncAPI v3 named messages: + +```yaml +asyncapi: 3.0.0 +channels: + userSignedUp: + address: user/signedup + messages: + UserSignedUpMessage: + payload: + type: object + properties: + displayName: + type: string + email: + type: string +``` + +Generated model: `UserSignedUpMessagePayload` (derived from message name `UserSignedUpMessage`) + #### Hierarchical naming for nested schemas Nested schemas in properties, `allOf`, `oneOf`, `anyOf`, `dependencies`, and `definitions` now receive hierarchical names based on their parent schema and property path. diff --git a/test/processors/AsyncAPIInputProcessor.spec.ts b/test/processors/AsyncAPIInputProcessor.spec.ts index b706e3d2b0..3558ea4514 100644 --- a/test/processors/AsyncAPIInputProcessor.spec.ts +++ b/test/processors/AsyncAPIInputProcessor.spec.ts @@ -1392,6 +1392,162 @@ describe('AsyncAPIInputProcessor', () => { expect(commonInputModel.models['EventsStreamPayload']).toBeDefined(); }); + test('should use message name instead of AnonymousSchema for named message with anonymous payload (issue #1996)', async () => { + // This is the exact scenario from issue #1996 + // When a message is defined in components/messages with a name (e.g., UserSignedUp) + // but its payload is an anonymous inline schema, the generated model should use + // the message name instead of AnonymousSchema_1 + const doc = { + asyncapi: '2.2.0', + info: { + title: 'Account Service', + version: '1.0.0', + description: + 'This service is in charge of processing user signups' + }, + channels: { + 'user/signedup': { + subscribe: { + message: { + $ref: '#/components/messages/UserSignedUp' + } + } + } + }, + components: { + messages: { + UserSignedUp: { + payload: { + type: 'object', + properties: { + displayName: { + type: 'string', + description: 'Name of the user' + }, + email: { + type: 'string', + format: 'email', + description: 'Email of the user' + } + } + } + } + } + } + }; + const processor = new AsyncAPIInputProcessor(); + const commonInputModel = await processor.process(doc); + const modelNames = Object.keys(commonInputModel.models); + + // Should NOT generate AnonymousSchema_1 as the model name + const anonymousModels = modelNames.filter((name) => + name.toLowerCase().includes('anonymous') + ); + expect(anonymousModels).toHaveLength(0); + + // Should generate a meaningful model name derived from message or channel context + expect(modelNames.length).toBeGreaterThan(0); + }); + + test('should use message name for anonymous payload in v3 with named messages', async () => { + const doc = { + asyncapi: '3.0.0', + info: { title: 'Account Service', version: '1.0.0' }, + channels: { + userSignedUp: { + address: 'user/signedup', + messages: { + UserSignedUpMessage: { + payload: { + type: 'object', + properties: { + displayName: { type: 'string' }, + email: { type: 'string', format: 'email' } + } + } + } + } + } + }, + operations: { + onUserSignedUp: { + action: 'receive', + channel: { $ref: '#/channels/userSignedUp' }, + messages: [ + { + $ref: '#/channels/userSignedUp/messages/UserSignedUpMessage' + } + ] + } + } + }; + const processor = new AsyncAPIInputProcessor(); + const commonInputModel = await processor.process(doc); + const modelNames = Object.keys(commonInputModel.models); + + // Should NOT generate AnonymousSchema as the model name + const anonymousModels = modelNames.filter((name) => + name.toLowerCase().includes('anonymous') + ); + expect(anonymousModels).toHaveLength(0); + + // Should use the message name for payload naming + expect( + commonInputModel.models['UserSignedUpMessagePayload'] + ).toBeDefined(); + }); + + test('should use message name for multiple named messages with anonymous payloads', async () => { + const doc = { + asyncapi: '2.0.0', + info: { title: 'Test', version: '1.0.0' }, + channels: { + events: { + subscribe: { + message: { + oneOf: [ + { + name: 'UserCreated', + payload: { + type: 'object', + properties: { + userId: { type: 'string' } + } + } + }, + { + name: 'UserUpdated', + payload: { + type: 'object', + properties: { + userId: { type: 'string' }, + updatedFields: { + type: 'array', + items: { type: 'string' } + } + } + } + } + ] + } + } + } + } + }; + const processor = new AsyncAPIInputProcessor(); + const commonInputModel = await processor.process(doc); + const modelNames = Object.keys(commonInputModel.models); + + // Should generate models without AnonymousSchema naming + expect(modelNames.length).toBeGreaterThan(0); + // Should have the oneOf wrapper for the channel + const anonymousModels = modelNames.filter((name) => + name.toLowerCase().includes('anonymousschema') + ); + // Anonymous schema naming should be avoided where possible + expect(anonymousModels.length).toBeLessThanOrEqual(0); + }); + test('should handle schema with only property name in context', async () => { const { document } = await parser.parse( JSON.stringify({ From 551770529278af0c566b28c4d4eff8afefe199d7 Mon Sep 17 00:00:00 2001 From: shyam-raghuwanshi Date: Sun, 22 Feb 2026 22:09:32 +0530 Subject: [PATCH 2/2] fix: add message.name() fallback for anonymous payload naming --- src/processors/AsyncAPIInputProcessor.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/processors/AsyncAPIInputProcessor.ts b/src/processors/AsyncAPIInputProcessor.ts index f8ee1087ed..11e3575985 100644 --- a/src/processors/AsyncAPIInputProcessor.ts +++ b/src/processors/AsyncAPIInputProcessor.ts @@ -334,14 +334,15 @@ export class AsyncAPIInputProcessor extends AbstractInputProcessor { // Add each individual message payload as a separate model const messageId = message.id(); - // Use message ID if available, otherwise use channel name + const messageName = message.name(); + // Use message ID if available, then message name, otherwise use channel name const contextId = messageId && !messageId.includes( AsyncAPIInputProcessor.ANONYMOUS_MESSAGE_PREFIX ) ? messageId - : contextChannelName; + : messageName || contextChannelName; const messageContext: SchemaContext = contextId ? { messageId: contextId } : {}; @@ -364,14 +365,15 @@ export class AsyncAPIInputProcessor extends AbstractInputProcessor { if (payload) { // Use message ID as context for better naming const messageId = message.id(); - // Use message ID if available, otherwise use channel name + const messageName = message.name(); + // Use message ID if available, then message name, otherwise use channel name const contextId = messageId && !messageId.includes( AsyncAPIInputProcessor.ANONYMOUS_MESSAGE_PREFIX ) ? messageId - : contextChannelName; + : messageName || contextChannelName; const messageContext: SchemaContext = contextId ? { messageId: contextId } : {}; @@ -398,10 +400,11 @@ export class AsyncAPIInputProcessor extends AbstractInputProcessor { for (const message of doc.allMessages()) { const payload = message.payload(); if (payload) { - // Use message id with 'Payload' suffix as the inferred name for the payload schema + // Use message id or message name with 'Payload' suffix as the inferred name for the payload schema // This avoids potential collisions with component schemas that might have the same name - const messageName = message.id() - ? `${message.id()}Payload` + const messageIdentifier = message.id() || message.name(); + const messageName = messageIdentifier + ? `${messageIdentifier}Payload` : undefined; addToInputModel(payload, messageName); }