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:
- Define path
/messages
- Tell that stream will be returned after
GET
request - Describe SSE headers in response:
- Content-Type: text/event-stream
- X-SSE-Content-Type: application/json
- 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:
- Define channel
/messages
- Add
publish
operation - Bind operation with
HTTP
:- Operation:
POST
- Type:
request
- Operation:
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:
- Define channel
/messages
- Add
subscribe
operation - Bind operation with
HTTP
:- Operation:
GET
- Type:
request
- Operation:
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: