Post

Comparing OpenAPI and AsyncAPI specifications for describing Server-Sent Events

Let’s assume that we have service which allows some users to send messages and others to receive them.

The most interesting moment for me is to compare the OpenAPI specification with the AsyncAPI specification in the next cases and compare results:

  • Use common Json Schemas to reduce needed work
  • Describe SSE connection with HTTP operation for messages sending

What will we document

Service which will:

  • Broadcast received messages through a Server-Sent Events (SSE) connection to any subscribed user
  • Receive messages for broadcasting trough HTTP Resource /messages

Diagram:

sequenceDiagram
  actor User
  participant App
  participant Sse emitter

  User->>App: Send message
  activate App
  Note over App, User: POST /messages HTTP/1.1<br/>Host: localhost:8080<br/>Content-Type: application/json<br/>Content-Length: 46<br/>{"message": "broadcast this message :rocket:"}
  activate Sse emitter
  App-->>Sse emitter: pass message
  App-->>User: 
  Note over App,User: HTTP/1.1 200 OK
  deactivate App

  User->>App: Request stream
  Note over App,User: GET /messages HTTP/1.1<br/>Host: localhost:8080
  activate App
  App-->>Sse emitter: subscribe
  App-->>User:  
  Note over App,User: HTTP/1.1 200 OK<br/>Content-Type: text/event-stream<br/>X-SSE-Content-Type: application/json<br/>transfer-encoding: chunked
  Sse emitter->>User: broadcast message
  deactivate App
  deactivate Sse emitter

Common part

Let’s collect all required schemas in one file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
schemas:
  Message:
    type: object
    additionalProperties: false
    required:
      - message
      - receivedAt
    properties:
      message:
        type: string
        example: 'broadcast this message :rocket:'
      receivedAt:
        type: string
        format: date-time
        example: '2023-08-31T15:28:21.283+00:00'
        description: Date-time when application received this message
  MessageToBroadcast:
    type: object
    additionalProperties: false
    required:
      - message
    properties:
      message:
        type: string
        example: 'broadcast this message :rocket:'
        description: Ordinary text which will be send

OpenAPI

Broadcast messages

Just basic declaration of POST method, nothing special at all

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
openapi: 3.0.3
info:
  version: 1.0.0
  title: Messages API
  description: >-
    Broadcasts received messages through an SSE connection to any subscribed
    user
servers:
  - url: 'http://localhost:8080'
paths:
  /messages:
    post:
      summary: Broadcast message
      operationId: broadcastMessage
      description: Send message to broadcast to any subscribed user
      tags:
        - messages
      requestBody:
        description: Message to broadcast
        required: true
        content:
          application/json:
            schema:
              $ref: './schemas.json#/schemas/MessageToBroadcast'
            example: 'broadcast this message :rocket:'
      responses:
        '200':
          description: message received

Subscribe to messages stream

More complicated part.

Unfortunately, community still figuring out how to describe events by OpenAPI in the right manner.

We can use this GitHub issue as a reference, to try to find approach to describe stream

We will use next steps for describing:

  1. Define path /messages
  2. Tell that stream will be returned after GET request
  3. Describe SSE headers in response:
    • Content-Type: text/event-stream
    • X-SSE-Content-Type: application/json
  4. Decide how to describe response content

Let’s assume that we can describe payload correctly as follows:

1
2
3
4
5
MessagesStream:
  type: array # it's stream of messages, so array is a good wrapper
  format: event-stream # SSE is about events, so it's event stream
  items:
    $ref: './schemas.json#/schemas/Message'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
openapi: 3.0.3
info:
  version: 1.0.0
  title: Messages stream API
  description: >-
    Broadcasts received messages through an SSE connection to any subscribed
    user
servers:
  - url: 'http://localhost:8080'
paths:
  /messages: # 1. Define path
    get: # 2. How to invoke stream
      summary: Subscribe to stream of messages
      operationId: messagesStreamSubscribe
      description: Receive all incoming messages
      tags:
        - messages
      responses:
        '200':
          description: Stream of messages
          headers: #3. SSE headers
            X-SSE-Content-Type:
              schema:
                type: string
                enum:
                  - application/json
            transfer-encoding:
              schema:
                type: string
                enum:
                  - chunked
          content: # 3. Right type of content-type
            text/event-stream:
              schema:
                $ref: '#/components/schemas/MessagesStream' # 4. Describe response content
components:
  schemas:
    MessagesStream:
      type: array
      format: event-stream
      items:
        $ref: './schemas.json#/schemas/Message'

Pub + Sub

Now let’s compose our contracts into one

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
openapi: 3.0.3
info:
  version: 1.0.0
  title: Messages API
  description: >-
    Broadcasts received messages through an SSE connection to any subscribed
    user
servers:
  - url: 'http://localhost:8080'
paths:
  /messages:
    get:
      summary: Subscribe to stream of messages
      operationId: messagesStreamSubscribe
      description: Receive all incoming messages
      tags:
        - messages
      responses:
        '200':
          description: Stream of messages
          headers:
            X-SSE-Content-Type:
              schema:
                type: string
                enum:
                  - application/json
            transfer-encoding:
              schema:
                type: string
                enum:
                  - chunked
          content:
            text/event-stream:
              schema:
                $ref: '#/components/schemas/MessagesStream'
    post:
      summary: Broadcast message
      operationId: broadcastMessage
      description: Send message to broadcast to any subscribed user
      tags:
        - messages
      requestBody:
        description: Message to broadcast
        required: true
        content:
          application/json:
            schema:
              $ref: '../schemas.json#/schemas/MessageToBroadcast'
            example: 'broadcast this message :rocket:'
      responses:
        '200':
          description: message received
components:
  schemas:
    MessagesStream:
      type: array
      format: event-stream
      items:
        $ref: './schemas.json#/schemas/Message'

AsyncAPI

Broadcast messages

Let’s describe POST request for message sending

We will use next steps for describing:

  1. Define channel /messages
  2. Add publish operation
  3. Bind operation with HTTP:
    • Operation: POST
    • Type: request
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
asyncapi: 2.6.0
info:
  title: Messages stream API
  description: >-
    Broadcasts received messages through an SSE connection to any subscribed
    user
  version: 1.0.0
servers:
  dev:
    url: 'http://localhost:8080'
    protocol: http
channels:
  /messages: # 1. Messages chanel
    description: Broadcast message
    publish: # 2. Publish message
      description: Send message to broadcast to any subscribed user
      message:
        bindings:
          http: # 3. Use this HTTP headers
            headers:
              type: object
              additionalProperties: false
              required:
                - Content-Type
              properties:
                Content-Type:
                  type: string
                  enum:
                    - application/json
        $ref: '#/components/messages/MessageToBroadcast'
      bindings: # 3. For POST operation
        http:
          type: request
          method: POST
components:
  messages:
    MessageToBroadcast:
      payload:
        $ref: './schemas.json#/schemas/MessageToBroadcast'

Subscribe to messages stream

Easiest part, is a messages stream description.

We will use next steps for describing:

  1. Define channel /messages
  2. Add subscribe operation
  3. Bind operation with HTTP:
    • Operation: GET
    • Type: request
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
asyncapi: 2.6.0
info:
  title: Messages stream API
  description: >-
    Broadcasts received messages through an SSE connection to any subscribed
    user
  version: 1.0.0
servers:
  dev:
    url: 'http://localhost:8080'
    protocol: http
channels:
  /messages: # 1. Define chanel
    description: Subscribe to stream of messages
    subscribe: # 2. Subscribe to it
      description: Receive all incoming messages
      message:
        bindings:
          http: # 3. Expect next HTTP headers
            headers:
              type: object
              additionalProperties: false
              required:
                - Content-Type
                - X-SSE-Content-Type
                - transfer-encoding
              properties:
                Content-Type:
                  type: string
                  enum:
                    - text/event-stream
                X-SSE-Content-Type:
                  type: string
                  enum:
                    - application/json
                transfer-encoding:
                  type: string
                  enum:
                    - chunked
        $ref: '#/components/messages/Message' # 3. With next events
      bindings:
        http: # 3. In response for GET request
          type: request
          method: GET
components:
  messages:
    Message:
      payload:
        $ref: './schemas.json#/schemas/Message'

Pub + Sub

Now let’s compose our contracts into one

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
asyncapi: 2.6.0
info:
  title: Messages stream API
  description: >-
    Broadcasts received messages through an SSE connection to any subscribed
    user
  version: 1.0.0
servers:
  dev:
    url: 'http://localhost:8080'
    protocol: http
channels:
  /messages:
    description: Broadcast message
    publish:
      description: Send message to broadcast to any subscribed user
      message:
        bindings:
          http:
            headers:
              type: object
              additionalProperties: false
              required:
                - Content-Type
              properties:
                Content-Type:
                  type: string
                  enum:
                    - application/json
        $ref: '#/components/messages/MessageToBroadcast'
      bindings:
        http:
          type: request
          method: POST
    subscribe:
      description: Receive all incoming messages
      message:
        bindings:
          http:
            headers:
              type: object
              additionalProperties: false
              required:
                - Content-Type
                - X-SSE-Content-Type
                - transfer-encoding
              properties:
                Content-Type:
                  type: string
                  enum:
                    - text/event-stream
                X-SSE-Content-Type:
                  type: string
                  enum:
                    - application/json
                transfer-encoding:
                  type: string
                  enum:
                    - chunked
        $ref: '#/components/messages/Message'
      bindings:
        http:
          type: request
          method: GET
components:
  messages:
    MessageToBroadcast:
      payload:
        $ref: './schemas.json#/schemas/MessageToBroadcast'
    Message:
      payload:
        $ref: './schemas.json#/schemas/Message'

Resume

Both specifications allow to you to describe Pub and Sub operations for our demo app, but with some tradeoffs

OpenAPI can’t offer canonical way how to describe stream.

For example:

1
2
3
4
5
6
7
{
  "type": "array",
  "format": "event-stream",
  "items": {
    "$ref": "../schemas.json#/schemas/Message"
  }
}

What does it mean? Stream of messages? Stream of arrays with messages?

From other side AsyncAPI as expected can’t offer flexible syntax for description of HTTP requests - headers, params location, status codes

It’s up to you to choose which specification to use, but looks like it’s better to use them both, instead of reinvent the wheel:

References

This post is licensed under CC BY 4.0 by the author.