Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions docs/migrations/version-5-to-6.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,61 @@ Generated model: `AnonymousSchema_1` or `<anonymous-message-1>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.
Expand Down
17 changes: 10 additions & 7 deletions src/processors/AsyncAPIInputProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
: {};
Expand All @@ -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 }
: {};
Expand All @@ -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);
}
Expand Down
156 changes: 156 additions & 0 deletions test/processors/AsyncAPIInputProcessor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1392,6 +1392,162 @@
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:

Check failure on line 1405 in test/processors/AsyncAPIInputProcessor.spec.ts

View workflow job for this annotation

GitHub Actions / Test NodeJS PR - ubuntu-latest

Delete `⏎···········`

Check failure on line 1405 in test/processors/AsyncAPIInputProcessor.spec.ts

View workflow job for this annotation

GitHub Actions / Test NodeJS PR - macos-latest

Delete `⏎···········`
'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({
Expand Down
Loading