Our journey through public API’s

Anatoly Chebanets
Universal Language
Published in
7 min readApr 22, 2019

--

Why public API docs are important?

Service oriented architecture and microservices continue to be a common trend in software development. Because of this, it’s imperative that every public API endpoint is described via developer-focused API documentation.

The key point is: systems are represented as a set of small services. Each service publishes public API endpoints so they can be used by a customer. Public API docs are the means to describe how to use the API endpoints that make up a service that supports a system. The quality of your API documentation can mean the difference between success and failure when a customer is integrating with your platform.

Why do we use Swagger (OpenAPI)?

A few years ago, Smartling evaluated many tools to host and maintain our public API documentation. Swagger stood out in our evaluation for two main reasons:

  1. GUI tool to document APIs. The Swagger UI allows you to document APIs using json or yaml files. The UI tool can be installed locally to run the editor, or by using their online version of the editor.
  2. Code generation. Swagger goes beyond a documentation GUI to provide toolsets for users (consumers of our API) to generate cURL references and API SDKs.

We initially rolled out our API documentation via Swagger v2.0, which was well received by our users. Many were astonished that they could generate simple API SDKs themselves. It also helped compress timelines to get their own tools into production since it provided a fast and simple way for them to prototype their integrations with our platforms.

Migrating from Swagger V2 to V3?

Over the years, we’ve continued to add new API endpoints to our offering. Some are very complex: input request body structure can be different depending on URL path parameter, and response DTO depends on that parameter. Further to that: endpoint responses can contain a list of DTO with different structures. We found that Swagger v2.0 was not able to handle this complexity.

Shortly after, Swagger announced the release of OpenAPI v3.0. New features were added, like the ability to describe response formats of different DTOs. They even introduced an online tool to convert descriptions from v2.0 to v3.0 — making the migration process as easy as uploading your json or yaml API description presentation, and the system converts it into the latest (3.0) format. That’s it.

In addition, alternative UI tools had started to enter the market, making the process to publish API descriptions even easier. We switched over to ReDoc (https://github.com/Rebilly/ReDoc), which supports all of the latest Swagger OpenAPI v3.0 features, and does so in a very user-friendly manner (i.e. anyOf, oneOf, “discriminator” — I will talk about them later. These are very magnificent keywords).

Below is a summary of the more complex API doc requirements we had and how OpenAPI v3.0 filled those gaps:

  • Declaring endpoint parameter once (creating shared parameter) — “schema”;
  • Injecting shared parameters by “$ref”;
  • Splitting requestBody and response DTOs on shared parts — declaring them as different schemas;
  • Combining the shared schemas by “allOf” to define schema for “request body”, response DTO;
  • If endpoint’s request body or response DTOs depend on input parameter, use the keyword “oneOf” to show that response DTO could be one from the set. If service’s response contains list of different DTO at the same time, use the keyword “anyOf”.
  • Smartling has a collection of different public API endpoint groups that related to specific features. We needed to put a common schema for all groups in one file, and specific schemas for certain groups in another file The “$ref” would need to point to a schema inside the current file or to another file.

I’ll describe this in more detail below:

  1. Import shared path parameter definition to avoid duplicate definitions:
components:
parameters:
accountUid:
name: accountUid
in: path
type: string
required: true
description: Unique identifier of corresponding account.
userUid:
name: userUid
in: path
type: string
required: true
description: Unique identifier of user.
pageUid:
name: pageUid
in: path
type: string
required: true
description: Unique identifier of page.
...
/accounts/{accountUid}/settings:
put:
parameters:
- $ref: '#/components/parameters/accountUid'
...
/accounts/{accountUid}/users/{userUid}
get:
parameters:
- $ref: '#/components/parameters/accountUid'
- $ref: '#/components/parameters/userUid
...
/accounts/{accountUid}/page/{pageUid}
post:
parameters:
- $ref: '#/components/parameters/accountUid'
- $ref: '#/components/parameters/pageUid
...

2. Split response DTO on shared and non-shared parts. Don’t duplicate the shared parts. Declare them as “schemas” and combine by “allOf”. See example below:

components:
schemas:
SuccessResponse:
type: object
required:
- code
properties:
code:
type: string
enum:
- SUCCESS
description: Indicates whether the response was successful or what error has occurred.
RestAPIResponse1:
type: object
properties:
response:
allOf:
- $ref: '#/components/schemas/SuccessResponse'
- type: object
properties:
data:
type: object
properties:
property11:
type: string
property12:
type: integer
RestAPIResponse2:
type: object
properties:
response:
allOf:
- $ref: '#/components/schemas/SuccessResponse'
- type: object
properties:
data:
type: object
properties:
property21:
type: string
property22:
type: integer
property23:
type: boolean

3. Service endpoint could generate different response DTO, which depend on input parameters. For the sample endpoint’s response “to get user’s settings”, it depends on the user role: admin user settings are different than ordinal user settings (for sample guest). It needs the declaration separated DTO for admin and guest. Use “oneOf” to represent list of DTOs for each user role (the attribute “title” is text label of certain user role). This means only one DTO from the list is present in the response.

components:
schemas:
ResponseDTO1:
title: admin settings
type: object
properties:
property11:
type: string
property12:
type: string
ResponseDTO2:
title: guest settings
type: object
properties:
property21:
type: string
property22:
type: string
RestAPIResponse:
type: object
properties:
response:
allOf:
- $ref: '../common.yaml#/components/.../SuccessResponse'
- type: object
properties:
data:
oneOf:
- $ref: '#/components/.../ResponseDTO1'
- $ref: '#/components/.../ResponseDTO2'

4. This example represents the ability to describe different DTO in a list (anyOf). Meaning, the response contains different DTO descriptions in a list.

components:
schemas:
ResponseDTO1:
title: admin settings
type: object
properties:
property11:
type: string
property12:
type: string
ResponseDTO2:
title: guest settings
type: object
properties:
property21:
type: string
property22:
type: string
RestAPIResponse:
type: object
properties:
response:
allOf:
- $ref: '../common.yaml#/components/.../SuccessResponse'
- type: object
properties:
data:
type: object
properties:
totalCount:
type: integer
items:
type: array
items:
anyOf:
- $ref: '#/components/.../ResponseDTO1'
- $ref: '#/components/.../ResponseDTO2'

5. These are simplified examples from real situations. They represent all approaches described above. The schemas could be defined in different files to split them on independent logical groups (areas):

<openapi.yaml>

path:
/translation-quality-api/v2/accounts/{accountUid}/
check-types/{checkTypeCode}/settings:
$ref: './check_paths.yaml#/x-paths/
account_check_types_checkType_settings'
<check_paths.yaml>
x-paths:
account_check_types_checkType_settings:
put:
parameters:
- $ref: '../common.yaml#/.../parameters/accountUid'
- $ref: './common.yaml#/.../parameters/checkTypeCode
requestBody:
description: ''
required: true
content:
application/json:
schema:
allOf:
$ref: '../common.yaml#/components/.../
AccounCheckTypeApplySettingsRequestOneOf'
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/
AccountCheckTypeApplySettingsResponse'
get:
parameters:
- $ref: './api_common.yaml#/components/parameters/accountUid'
- $ref: './tqc_common.yaml#/components/parameters/checkTypeCode'
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/
AccountCheckTypeApplySettingsResponse'
components:
schemas:
AccountCheckTypeApplySettingsRequestOneOf:
oneOf:
- $ref:
'./whitespaces.yaml#/components/schemas/
AccountCheckTypeApplySettingsRequest'
- $ref: './spellcheck.yaml#/components/schemas/
AccountCheckTypeApplySettingsRequest'
AccountCheckTypeApplySettingsDTOResponseOneOf:
oneOf:
- $ref: './whitespaces.yaml#/components/schemas/
AccountCheckTypeApplySettingsResponse'
- $ref: './spellcheck.yaml#/components/schemas/
AccountCheckTypeApplySettingsResponse'
AccountCheckTypeApplySettingsResponse:
type: object
properties:
response:
allOf:
- $ref: '../common.yaml#/components/.../SuccessResponse'
- type: object
properties:
data: $ref:'../common.yaml#/components/.../
AccountCheckTypeApplySettingsDTOResponseOneOf'
<common.yaml>
components:
parameters:
accountUid:
name: accountUid
in: path
type: string
required: true
description: Unique identifier of corresponding account.
schemas:
SuccessResponse:
type: object
required:
- code
properties:
code:
type: string
enum:
- SUCCESS
description: Indicates whether the response was successful or what error has occurred.
<tqc_common.yaml>
components:
parameters:
checkTypeCode:
name: checkTypeCode
in: path
required: true
schema:
type: string
enum:
- EMOJI_CONSISTENCY
- SPELLCHECK
description: check type code
<emoji_consistency.yaml>
components:
schemas:
EmojiConsistencySettingsDTO:
type: object
properties:
noEmojisAllowedInTarget:
type: boolean
description: No emoji is allowed in target
example: false

AccountCheckTypeApplySettingsRequest:
title: EMOJI_CONSISTENCY
allOf:
- $ref: '#/components/schemas/EmojiConsistencySettingsDTO'
- type: object
required:
- noEmojisAllowedInTarget
AccountCheckTypeApplySettingsDTOResponse:
title: EMOJI_CONSISTENCY
allOf:
- $ref: '#/components/schemas/EmojiConsistencySettingsDTO
<spellcheck.yaml>
components:
schemas:
SpellcheckSettingsDTO:
type: object
properties:
skipIfWordInUserDictionary:
type: boolean
description: Skip if word is in user dictionary.
example: false
skipIfWordStartsWithCapitalLetter:
type: boolean
description: Skip if word starts with capital letter.
example: false
AccountCheckTypeApplySettingsRequest:
title: SPELLCHECK
allOf:
- $ref:'#/components/schemas/SpellcheckSettingsDTO'
- type: object
required:
- skipIfWordInUserDictionary
- skipIfWordStartsWithCapitalLetter
AccountCheckTypeApplySettingsDTOResponse:
title: SPELLCHECK
allOf:
- $ref: '#/components/schemas/SpellcheckSettingsDTO'

Tool limitations and improvements

With every tool there are limitations. Here are a few we encountered:

  • It’s not possible to inject the same schema twice in the same schema;
  • Discriminator doesn’t support references like “./file_1.yaml#/components/schemas/AccountCheckTypeDTO”;
  • Issue with examples;
  • Yaml file validation;

How we worked around limitations

Let’s talk about how we worked around these limitations of the tool:

  1. It’s not possible to inject the same schema twice in the same schema:

If we inject the same schema ”severityLevelResponse” twice, the tool will raise an error.

RestResponse:
type: object
properties:
data:
allOf:
- $ref: '#/components/.../severityLevelResponse'
- type: object
properties:
subTypes:
type: array
items:
allOf:
- $ref: '#/components/.../severityLevelResponse'
- $ref: '#/components/.../checkSubTypeResponse'

To overcome this, we needed to clone the definition and inject the duplicated schema

components:
schemas:
RestResponse:
type: object
properties:
data:
allOf:
- $ref: '#/components/.../severityLevelResp’
- type: object
properties:
subTypes:
type: array
items:
allOf:
- $ref: '#/components/.../severityLevelResp1'
- $ref: '#/components/.../checkSubTypeResp'
severityLevelResp:
type: object
properties:
severityLevelCode:
type: string
$ref: '#/components/schemas/enum/severityLevelCodeEnum'
description: severity level code
example: "MEDIUM"
severityLevelResp1:
type: object
properties:
severityLevelCode:
type: string
$ref: '#/components/schemas/enum/severityLevelCodeEnum'
description: severity level code
example: "MEDIUM"
checkSubTypeResp:
type: object
properties:
checkSubTypeCode:
type: string
$ref: '#/components/schemas/enum/checkSubtypeEnum'
description: check subtype code
example: "EMOJI_ADDED"
name:
type: string
description: check subtype name
example: "Emoji added"

enum:
severityLevelCodeEnum:
enum:
- DISABLED
- LOW
- MEDIUM
- HIGH
checkSubtypeEnum:
enum:
- EMOJI_ADDED
- EMOJI_DELETED
- EMOJI_PRESENT

2. Discriminator:

In common case schema with discriminator is declared like below

CheckTypeSettingBase:
type: object
required:
- checkTypeCode
discriminator:
propertyName: checkTypeCode
mapping:
LEADING_TRAILING_SPACES: '#/.../.../LEADING_TRAILING_SPACES'
ORIGINAL_EQUALS_TARGET: '#/.../.../ORIGINAL_EQUALS_TARGET'

There is a small exception: the path “#/…/…/ORIGINAL_EQUALS_TARGET” couldn’t be like “file_1.yaml#/…/…/ORIGINAL_EQUALS_TARGET” if ORIGINAL_EQUALS_TARGET is declared in another file.

To work around this: declare the schema in the main file “openapi.yaml” with reference to another file:

ORIGINAL_EQUALS_TARGET:
$ref: './file_1.yaml#/.../.../AccountCheckTypeEffectiveDTO'

3. Issue with examples.

The example below doesn’t work properly because the tool doesn’t resolve the references “$ref: ‘file_1.yaml#/…/…/endpoint_example_1’” and “$ref: ‘file_1.yamll#/…/…/endpoint_example_2’”.

endpoint_example:
example_1:
value:
$ref: 'file_1.yaml#/components/examples/endpoint_example_1’
example_2:
value:
$ref: 'file_1.yamll#/components/examples/endpoint_example_2'


endpoint_example_1:
$ref: '#/components/examples/endpoint_example_shared'
endpoint_example_2:
$ref: '#/components/examples/endpoint_example_shared'
endpoint_example_shared:
response:
data:
property1: 100
property2: true

To deal with this we needed duplicate examples:

endpoint_example_1:
response:
data:
property1: 100
property2: true
endpoint_example_2:
response:
data:
property1: 100
property2: true

4. Yaml file validation: if you use the yaml multi-file approach, the tool doesn’t validate all files. It validates only the main “openapi.yaml”, even though the preview works. We have yet to find a solution for this.

Conclusion

As the emphasis on Service Oriented Architecture and Microservices continues to grow, the need for powerful developer-focused API documentation will become even more important. This means comprehensive information, with real examples that bring your APIs to life. Swagger’s OpenAPI v3.0 and ReDoc provided us with a toolset that enabled us to manage a large quantity and variety of complex public API endpoints at scale, and provide rich context to our APIs. These tools may help you to be equally successful documenting your own APIs.

--

--