Skip to content

feat(asyncapi): Support AsyncAPI 3.0 Operation Reply Object for request/reply rendering #16653

Description

@devin-ai-integration

Summary

Fern does not parse or render the AsyncAPI 3.0 Operation Reply Object. The reply field on Operation Objects is silently ignored during import, meaning correlated responses for request/reply (RPC-style) operations are absent from rendered WebSocket documentation pages.

The reply Object in AsyncAPI 3.0

The reply field was introduced in AsyncAPI 3.0.0 as a first-class, optional property on the Operation Object (JSON Schema source). It enables the request/reply pattern — the spec describes it as:

"Describes the reply part that MAY be applied to an Operation Object. If an operation implements the request/reply pattern, the reply object represents the response message."

It has its own dedicated schema (Operation Reply Object) with three properties:

Field Type Description
address Operation Reply Address Object | Reference Where to send the reply (runtime expression)
channel Reference Object Which channel carries the reply
messages [Reference Object] Which message schemas are valid replies

This field did not exist in AsyncAPI 2.x. It was added in 3.0.0 specifically to model protocols like JSON-RPC, AMQP RPC, and any request/response pattern over messaging.

Why reply Matters — It Is Not the Same as receive

AsyncAPI 3.0 distinguishes two fundamentally different server-to-client message patterns:

reply (on a send operation): A correlated response to a specific request. The reply only exists because the client sent a request — it is causally linked, 1:1.

operations:
  callingDial:
    action: send
    channel: { $ref: "#/channels/callingDial" }
    messages:
      - $ref: "#/channels/callingDial/messages/callingDialRequest"
    reply:
      channel: { $ref: "#/channels/callingDial" }
      messages:
        - $ref: "#/channels/callingDial/messages/callingDialResponse"

receive (standalone operation): An autonomous, unsolicited event from the server. It can arrive at any time with no correlation to a prior request.

operations:
  onCallStateChanged:
    action: receive
    channel: { $ref: "#/channels/calling" }
    messages:
      - $ref: "#/channels/calling/messages/callStateEvent"
Aspect send + reply receive
Trigger Direct response to a specific request Server pushes independently
Correlation 1:1 with the send operation None
Semantic meaning "Call this, get that back" "Listen for these events"
Analogous to HTTP request → response Webhook / SSE event

Without reply support, there is no way for a spec author to express this distinction. Both correlated responses and autonomous events are flattened into receive, losing the request/reply semantics that the spec was designed to convey.

Current Behavior

Given this spec:

asyncapi: 3.0.0
info:
  title: RPC Test API
  version: 1.0.0
servers:
  production:
    host: ws.example.com
    protocol: wss
channels:
  test:
    address: /
    messages:
      commandRequest:
        payload:
          type: object
          properties:
            method: { type: string, const: "test.command" }
            params: { type: object }
      commandResponse:
        payload:
          type: object
          properties:
            result: { type: object }
      asyncEvent:
        payload:
          type: object
          properties:
            event_type: { type: string, const: "test.event" }
            data: { type: object }
operations:
  testCommand:
    action: send
    channel: { $ref: "#/channels/test" }
    messages:
      - $ref: "#/channels/test/messages/commandRequest"
    reply:
      channel: { $ref: "#/channels/test" }
      messages:
        - $ref: "#/channels/test/messages/commandResponse"
  onAsyncEvent:
    action: receive
    channel: { $ref: "#/channels/test" }
    messages:
      - $ref: "#/channels/test/messages/asyncEvent"

What renders:

  • Send section → commandRequest (the request)
  • Receive section → asyncEvent (the autonomous event)
  • commandResponse does not render anywhere — it is completely absent from the page

The reply block and its referenced message are silently dropped.

Additionally, if the reply targets a different channel (e.g., a ping operation replies on a pong channel), the reply channel is skipped entirely with: Skipping AsyncAPI channel pong as it does not qualify for inclusion (no headers, query params, or operations). This happens because no send/receive operation references the reply-only channel.

Workaround and Why It Is Insufficient

The only workaround is to model the response as a separate receive operation:

operations:
  testCommand:
    action: send
    messages: [commandRequest]
  onTestCommandResponse:        # Workaround — pretend the response is an event
    action: receive
    messages: [commandResponse]
  onAsyncEvent:
    action: receive
    messages: [asyncEvent]

This renders both messages, but introduces two problems:

  1. Semantic loss: onTestCommandResponse and onAsyncEvent are now structurally identical — both are receive operations. The rendered page shows them both under "Receive" with no distinction. A reader cannot tell which is a correlated response to testCommand and which is an independent event. For APIs with many RPC methods plus async events on the same channel, this is confusing.

  2. Spec incorrectness: The receive workaround tells consumers "this message can arrive at any time, unprompted" — which is false for a correlated response. Any tooling that processes the spec (SDK generators, validators, documentation) will treat it as an autonomous event. Spec authors are forced to misrepresent their API semantics to get documentation to render.

  3. Duplication: To keep the spec correct for non-Fern tooling, authors must maintain BOTH the reply block (for spec compliance) AND a duplicate receive operation (for Fern rendering) — doubling the operation count per RPC method.

Expected Behavior

  1. Parse the reply field from AsyncAPI 3.0 Operation Objects during import.

  2. Render reply messages on the WebSocket channel page, visually distinct from receive messages. For example:

    ┌─────────────────────────────────────┐
    │ test.command                        │
    ├─────────────────────────────────────┤
    │ ► Send                              │
    │   commandRequest                    │
    │                                     │
    │ ◄ Reply                             │
    │   commandResponse                   │
    │                                     │
    │ ◄ Receive                           │
    │   asyncEvent                        │
    └─────────────────────────────────────┘
    

    The Reply section communicates "this is what you get back when you send the request." The Receive section communicates "these events may arrive independently."

  3. Support cross-channel replies: When reply.channel references a different channel than the operation, include the reply messages on the operation's page rather than skipping the reply channel.

Real-World Use Case

This is a practical blocker for documenting JSON-RPC over WebSocket APIs. Any protocol where the client sends a JSON-RPC request and receives a correlated JSON-RPC response, plus independent server-pushed events over the same connection, needs reply support. APIs with 50+ RPC methods are especially affected — each method's response is either missing from docs or incorrectly modeled as an autonomous event. The reply object is the spec-correct way to model this, but Fern's lack of support forces authors to either:

  • Drop the response from docs entirely (bad DX)
  • Model responses as receive events (semantically wrong, confusing for readers)
  • Maintain duplicate operations (spec bloat)

Spec References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions