Designing Api With Open Api Specification 3 OAS 3.0

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.

Basic Structure

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:

If we paste the specification in the Swagger editor, we will get the following UI:

basic_spec

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'

Paths Object

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:

  1. retrieve all values,
  2. add a new value,
  3. clear all values,
  4. retrieve a specific value given an identifier

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:

  • /values
    • GET
    • POST
  • /values/{id}
    • GET
    • PUT
    • DELETE

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.

Components 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.

Human Friendly Specification

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:

  • /openapi
    • openapi.yml
    • /paths
      • values.yml
    • /schemas
      • my-value.yml

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.

Final Specification

Conclusion

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!

External Sources

Designed, built and maintained by Kimserey Lam.