Jul 5th, 2019 - written by Kimserey with .
The OpenAPI Specification (OAS) is a standard describing the interface of restful APIs. It provides a way for developers to understand quickly the functionalities provided by an API, and also provides a way to automate tasks around discovery, testing and also generation of client SDK in multiple languages. Each language plus web framework combination provides some sort libraries enabling auto-generation of the OpenAPI specification, for example in DotNetCore
we have Swashbuckle.AspNetCore
or in Python
with Flask
we have Flasgger
. Today we will take the opposite idea to auto-generation and craft the specification by hand. The technology agnositic aspect of the OpenAPI specification makes it an incredible tool to design APIs and brainstorm at the interface level prior writing a single line ofe code. We will look into what constitute a specification and how we can arrange the specification in a human friendly way.
The specification can be written either in Json or YAML format. For readability we will use YAML, but we will see later how to combine both YAML definition and Json. The specification is recommended to be named either openapi.json
or openapi.yml
.
The basic structure needs to contain the required fields openapi
, info
and paths
. Therefore an example of a basic structure would be:
1
2
3
4
5
6
7
8
9
10
11
12
openapi: 3.0.2
info:
title: My API
version: 1.0.0
paths:
/values:
get:
responses:
'200':
description: 'test'
openapi
defines the OpenAPI specification version 3.0.2
,info
provides information about the API,paths
defines the paths available in the API.Each of the properties can be found on the specification, the main structure is defined by the OpenAPI Object where required fields have a description starting with REQUIRED
.
Each property has a corresponding schema available:
info
🠮 Info Object,paths
🠮 Paths Object.If we paste the specification in the Swagger editor, we will get the following UI:
In this example we have described an API called My API
with a single endpoint GET /values
abd specified a single possible response 200
. By default the Swagger UI will use the current host as the host for all request.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
openapi: 3.0.2
info:
title: My API
version: 1.0.0
servers:
- url: 'http://localhost:5000/api'
description: Development server
paths:
/values:
get:
responses:
'200':
description: 'test'
The main content of the API can be defined under paths
. Paths
is an array of Path Item Object
.
Under a path item, the supported HTTP methods can be specified with a schema of Operation Object
.
Let’s say we were to design an API which would allow to interact with values
. Where each value with have an identifier and a value. We would be able to:
A complete specification example would look as such:
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
openapi: 3.0.2
info:
title: My API
version: 1.0.0
servers:
- url: 'http://localhost:5000/api'
description: >-
Development server
paths:
/values:
get:
tags:
- Values
responses:
'200':
description: >-
Returns a list of values
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: string
value:
type: integer
example:
- id: "001"
value: 1
- id: "002"
value: 2
post:
tags:
- Values
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
id:
type: string
value:
type: integer
examples:
first-example:
value:
id: "001"
value: 1
second-example:
value:
id: "002"
value: 2
responses:
'200':
description: >-
Succeeded posting a value
/values/{id}:
get:
tags:
- Single values
parameters:
- name: id
in: path
required: true
schema:
type: string
examples:
first-id:
value:
'001'
second-id:
value:
'002'
responses:
'200':
description: >-
Returns a single value
content:
application/json:
schema:
type: object
properties:
id:
type: string
value:
type: integer
example:
id: "001"
value: 1
put:
tags:
- Single values
parameters:
- name: id
in: path
required: true
schema:
type: string
examples:
first-id:
value:
'001'
second-id:
value:
'002'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
value:
type: integer
examples:
first-example:
value:
value: 10
second-example:
value:
value: 20
responses:
'200':
description: >-
Succeeded updating value
delete:
tags:
- Single values
parameters:
- name: id
in: path
required: true
schema:
type: string
examples:
first-id:
value:
'001'
second-id:
value:
'002'
responses:
'200':
description: >-
Succeeded deleting value
This specification provides the definition of the endpoint:
Looking at POST /values
,
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
post:
tags:
- Values
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
id:
type: string
value:
type: integer
examples:
first-example:
value:
id: "001"
value: 1
second-example:
value:
id: "002"
value: 2
responses:
'200':
description: >-
Succeeded posting a value
we defined tags
to group the endpoint under a category and we defined a requestBody
and responses
.
RequestBody
defines the body of the POST request. The content
property is a map with media type as key and Media Type Object
as value.
In this example, our restful API Values only deals with application/json
format therefore we only specify a single media type. The media type object then expects a schema which defines the schema of the request body.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
application/json:
schema:
type: object
properties:
id:
type: string
value:
type: integer
examples:
first-example:
value:
id: "001"
value: 1
second-example:
value:
id: "002"
value: 2
A schema is a subset of Json Schema. Some of the properties are taken directly from the json specification while other properties were readjusted for the purpose of OpenAPI.
Examples
is a map with the example name as key as the example body as Example Object
.
Lastly the responses
defines the different response of the path, with a map taking as key the status code and with value a Response Object
then just like the resquestBody
, it exposes a content
, a map of media type with Media Type Object
.
GET /values
and GET /value/{id}
return Json values of almost identical schenas. This is a common behaviour of APIs where endpoints might return objects composed of other objects themselves retrievable through other endpoints.
In this situation, we can use components
to abstract the schema definiton of the value.
1
2
3
4
5
6
7
8
9
components:
schemas:
MyValue:
type: object
properties:
id:
type: string
value:
type: integer
Then from the paths, we use the reference keyword $ref
and reference the component within the document by prefixing it with #/
.
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
paths:
/values:
get:
tags:
- Values
responses:
'200':
description: >-
Returns a list of values
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/MyValue'
example:
- id: "001"
value: 1
- id: "002"
value: 2
/values/{id}:
get:
tags:
- Single values
parameters:
- name: id
in: path
required: true
schema:
type: string
examples:
first-id:
value:
'001'
second-id:
value:
'002'
responses:
'200':
description: >-
Returns a single value
content:
application/json:
schema:
$ref: '#/components/schemas/MyValue'
example:
id: "001"
value: 1
Parameters, request body, responses and examples can also be shared under components.
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
components:
schemas:
MyValue:
type: object
properties:
id:
type: string
value:
type: integer
parameters:
Id:
name: id
in: path
required: true
schema:
type: string
examples:
first-id:
value:
'001'
second-id:
value:
'002'
examples:
Example1:
value:
id: "001"
value: 1
Example2:
value:
id: "002"
value: 2
And we can then reuse the schemas, parameter and examples defined in components
.
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
openapi: 3.0.2
info:
title: My API
version: 1.0.0
servers:
- url: 'http://localhost:5000/api'
description: >-
Development server
paths:
/values:
get:
tags:
- Values
responses:
'200':
description: >-
Returns a list of values
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/MyValue'
post:
tags:
- Values
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MyValue'
examples:
1:
$ref: '#/components/examples/Example1'
2:
$ref: '#/components/examples/Example2'
responses:
'200':
description: >-
Succeeded posting a value
/values/{id}:
get:
tags:
- Single values
parameters:
- $ref: '#/components/parameters/Id'
responses:
'200':
description: >-
Returns a single value
content:
application/json:
schema:
$ref: '#/components/schemas/MyValue'
examples:
1:
$ref: '#/components/examples/Example1'
2:
$ref: '#/components/examples/Example2'
put:
tags:
- Single values
parameters:
- $ref: '#/components/parameters/Id'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
value:
type: integer
example:
value: 10
responses:
'200':
description: >-
Succeeded updating value
delete:
tags:
- Single values
parameters:
- $ref: '#/components/parameters/Id'
responses:
'200':
description: >-
Succeeded deleting value
components:
schemas:
MyValue:
type: object
properties:
id:
type: string
value:
type: integer
parameters:
Id:
name: id
in: path
required: true
schema:
type: string
examples:
1:
value: '001'
2:
value: '002'
examples:
Example1:
value:
id: "001"
value: 1
Example2:
value:
id: "002"
value: 2
Components
also specifies the authentication mechanism available in the API under securitySchemes
which is a map taking as key the name of the scheme and as value a Security Scheme Object. For example the description of an API protected behind a JWT bearer token would be:
1
2
3
4
5
6
components:
securitySchemes:
bearer:
type: http
scheme: bearer
bearerFormat: JWT
Then under the path we can specify the security.
1
2
3
get:
security:
- bearer: []
Bearer
refers to the key specified under securitySchemes
and the empty array []
is meant to specify the scope requested for authorization. The scope are used in OAuth 2.0 context, for Bearer authorization it isn’t required.
We saw how $ref
could be used to reuse definitions of parameters, schemas or examples defined under components
. But $ref
can also be used to reference files containing specifications and/or also URI. Our example has grown to be a specification of about 140 lines even though we only 5 endpoints, we can imagine how a specification grow in proportion to endpoints added. To keep the specification easy to maintain, we can break specification in pieces where Reference Object can be used, Path Item Object
and Schema Object
.
We can then remodel our specification as such:
As we split the specification in multiple paths, the swagger editor will no longer work as it expects the specification to be contained in a single file. To still be able to visualize the swagger UI, we can get the official Swagger UI repository.
1
git clone https://github.com/swagger-api/swagger-ui.git
The /dist
folder contains the latest distribution of the Swagger UI, we can then copy the distribution and place it under the same folder where our /openapi
folder is. And use a tool like http-server
to host the Swagger UI.
1
2
npm install -g http-server
http-server -c-1
-c-1
disables the caching so that our YAML configurations would be cached. We can then split our files and use $ref
to reference the API. We start by openapi.yml
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
openapi: 3.0.2
info:
title: My API
version: 1.0.0
servers:
- url: 'http://localhost:5000'
description: >-
Development server
paths:
/api/values:
$ref: '/openapi/paths/values.yml#%2Fapi%2Fvalues'
/api/values/{id}:
$ref: '/openapi/paths/values.yml#%2Fapi%2Fvalues%2F%7Bid%7D'
Then we have the schema my-value.yml
:
1
2
3
4
5
6
7
8
9
type: object
properties:
id:
type: string
value:
type: integer
required:
- id
- value
One of the benefit of splitting the files is that it allows us to choose between YAML or JSON, we could create my-value.json
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"value": {
"type": "integer"
}
},
"required": [
"id",
"value"
]
}
This schema can then be used to validate the properties present in the schema using a JSON Schema validator. Lastly we define the paths
:
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/api/values:
get:
tags:
- Values
responses:
'200':
description: >-
Returns a list of values
content:
application/json:
schema:
type: array
items:
$ref: '/openapi/schemas/my-value.yml'
post:
tags:
- Values
requestBody:
required: true
content:
application/json:
schema:
$ref: '/openapi/schemas/my-value.yml'
examples:
1:
$ref: '#/components/examples/Example1'
2:
$ref: '#/components/examples/Example2'
responses:
'200':
description: >-
Succeeded posting a value
/api/values/{id}:
get:
tags:
- Single values
parameters:
- $ref: '#/components/parameters/Id'
responses:
'200':
description: >-
Returns a single value
content:
application/json:
schema:
$ref: '/openapi/schemas/my-value.yml'
example:
id: '100'
value: 100
put:
tags:
- Single values
parameters:
- $ref: '#/components/parameters/Id'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
value:
type: integer
example:
value: 10
responses:
'200':
description: >-
Succeeded updating value
delete:
tags:
- Single values
parameters:
- $ref: '#/components/parameters/Id'
responses:
'200':
description: >-
Succeeded deleting value
components:
parameters:
Id:
name: id
description: >-
The identifier of the value to act on
in: path
required: true
schema:
type: string
example: 10
examples:
Example1:
value:
id: "001"
value: 100
Example2:
value:
id: "002"
value: 2
We define the reusable components
as part of values.yml
so that it can be reused across the file. And that completes today’s post.
Today we saw how to define an OpenAPI Specification in YAML. We started by looking at a basic structure, where we then dived into how to configure Path Item Objects, Schema Objects and Component Objets. We then completed the post by looking at how we could download the Swagger UI locally with the latest version and supporting the latest specification version. The OpenAPI Specification provides a powerful way of expressing our intent when designing APIs prior writing a single line of code. And knowing its specificities opens a lot of possibilities for expressing requests, responses and examples that isn’t apparent when using autogeration libraries (one of the points at the heart of the debate of code-first vs schema-first). I hope you liked this post and I see you on the next one!