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!