Why Your OpenAPI Spec Sucks

A mix of anticipation and dread washes over me as I open a new inbound email with an attached specification file. With a heavy sigh, I begin scrolling through its contents, only to be greeted by disappointment yet again.
The API request bodies lack essential details: the actual properties of the HTTP call, making it impossible to determine expectations and behavior. The empty objects provided, masquerading as request bodies, offer no guidance to API consumers. Moreover, the inadequate specification file hinders the use of external libraries for validation, analysis or autogeneration of output (like API mocking, testing or Liblab’s auto SDK generation).
After encountering hundreds of specification files (referred to as specs) in my role at Liblab, I’ve come to the conclusion that most spec files are in varying degrees of incompletion. Some completely disregard the community standard and omit crucial information, while others could use some tweaking and refinement. This has inspired me to write this post to help you enhance the quality of your spec files. This also aligns with making my job easier.
In the upcoming sections, we’ll go over three common issues that make your OpenAPI spec fall short and examine possible solutions for them. By the end of this post you’ll be able to elevate your OpenAPI spec, making it more user-friendly for API consumers, including developers, QA engineers and other stakeholders.
Three Reasons Why
You’re Still Using Swagger
Look, I get it. A lot of us still get confused about the differences between Swagger and OpenAPI. To make things simple, you can think of Swagger as the former name of OpenAPI. Many tools are still using the word “Swagger” in their names, but this is primarily due to the strong association and recognition that the term Swagger has gained within the developer community.
If your “Swagger” spec is actually an OpenAPI spec (indicated by the presence of “openapi: 3.x.x” at the beginning), all you need to do is update your terminology.
If you’re actually using a Swagger spec (a file that begins with “swagger: 2.0”), it’s time to consider an upgrade. Swagger has certain limitations compared to OpenAPI 3, and as newer versions of OpenAPI are released, transitioning will become increasingly challenging.
Notable differences:
- OpenAPI 3 has support for oneOf and anyOf that Swagger does not provide. Let’s look at an example:
12345678910111213141516171819202122openapi: 3.0.0info:title: Payment APIversion: 1.0.0paths:/payments:post:summary: Create a paymentrequestBody:required: truecontent:application/json:schema:oneOf:- $ref: '#/components/schemas/CreditCardPayment'- $ref: '#/components/schemas/OnlinePayment'- $ref: '#/components/schemas/CryptoPayment'responses:'201':description: Created'400':description: Bad Request
In OpenAPI 3, you can explicitly define that the requestBody for a /payments POST call can be one of three options: CreditCardPayment, OnlinePayment or CryptoPayment. However, in Swagger you would need to create a workaround by adding an object with optional fields for each payment type:
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 |
swagger: '2.0' info: title: Payment API version: 1.0.0 paths: /payments: post: summary: Create a payment consumes: - application/json produces: - application/json parameters: - name: body in: body required: true schema: $ref: '#/definitions/Payment' responses: '201': description: Created '400': description: Bad Request definitions: Payment: type: object properties: creditCardPayment: $ref: '#/definitions/CreditCardPayment' onlinePayment: $ref: '#/definitions/OnlinePayment' cryptoPayment: $ref: '#/definitions/CryptoPayment' # Make the properties optional required: [] CreditCardPayment: type: object # Properties specific to CreditCardPayment OnlinePayment: type: object # Properties specific to OnlinePayment CryptoPayment: type: object # Properties specific to CryptoPayment |
This example does not resemble the OpenAPI 3 implementation fully as the API consumer has to specify the type they are sending through a property field, and they also might send more than one since they are all marked optional. This approach lacks the explicit validation and semantics provided by the oneOf keyword in OpenAPI 3.
- In OpenAPI you can describe many server URLs, while in Swagger you’re bound to only one:
123456789101112131415161718192021{"swagger": "2.0","info": {"title": "Sample API","version": "1.0.0"},"host": "api.example.com","basePath": "/v1",...}openapi: 3.0.0info:title: Sample APIversion: 1.0.0servers:- url: <http://api.example.com/v1>description: Production Server- url: <https://sandbox.api.example.com/v1>description: Sandbox Server...
You’re Not Using Components
One way to make an OpenAPI spec more readable is by removing any unnecessary duplication, the same as a programmer would with their code. If you find that your OpenAPI spec is too messy and hard to read, you might be underutilizing the components section. Components provide a powerful mechanism for defining reusable schemas, parameters, responses and other elements within your specification.
Let’s look at the following example that does not use 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 |
openapi: 3.0.0 info: title: Nested Query Example version: 1.0.0 paths: /users: get: summary: Get users with nested query parameters parameters: - name: filter in: query schema: type: object properties: name: type: string age: type: number address: type: object properties: city: type: string state: type: string country: type: string zipcode: type: string ... /user/{id}/friend: get: summary: Get a user's friend parameters: - name: id in: path schema: type: string - name: filter in: query schema: type: object properties: name: type: string age: type: number address: type: object properties: city: type: string state: type: string country: type: string zipcode: type: string ... |
The filter parameter in this example is heavily nested and can be challenging to follow. It is also used in its full length by two different endpoints. We can consolidate this behavior by leveraging component schemas:
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 |
openapi: 3.0.0 info: title: Nested Query Example with Schema References version: 1.0.0 paths: /users: get: summary: Get users with nested query parameters parameters: - name: filter in: query schema: $ref: '#/components/schemas/UserFilter' ... /user/{id}/friend: get: summary: Get a user's friend parameters: - name: id in: path schema: type: string - name: filter in: query schema: $ref: '#/components/schemas/UserFilter' ... components: schemas: UserFilter: type: object properties: name: type: string age: type: number address: $ref: '#/components/schemas/AddressFilter' AddressFilter: type: object properties: city: type: string state: type: string country: type: string zipcode: type: string |
The second example is clean and readable. By creating UserFilter and AddressFilter, we can reuse those schemas throughout the spec file, and if they ever change, we will only have to update them in one place.
You’re Not Using Descriptions, Examples, Formats or Patterns
You finally finished porting all your endpoints and models into your OpenAPI spec. It took you a while, but now you can finally share it with development teams, QA teams and even customers. Shortly after you share your spec with the world, the questions start arriving: “What does this endpoint do? What’s the purpose of this parameter? When should the parameter be used?”
Let’s look at this example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
openapi: 3.0.0 info: title: Sample API version: 1.0.0 paths: /data: post: summary: Upload user data requestBody: required: true content: application/json: schema: type: object properties: name: type: string age: type: integer email: type: string responses: '200': description: Successful response |
We can deduce from it that data needs to be uploaded, but questions remain: What specific data should be uploaded? Is it the data that pertains to the current user? Whose name, age and email do these attributes correspond to?
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 |
openapi: 3.0.0 info: title: Sample API version: 1.0.0 paths: /data: post: summary: Upload user data description: > Endpoint for uploading new user data to the system. This data will be used for personalized recommendations and analysis. Ensure the data is in a valid JSON format. requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: The name of a new user. age: type: integer description: The age of a new user. email: type: string description: The email address of a new user. responses: '200': description: Successful response |
You can’t always control how your API is structured, but you can control the descriptions you give it. Reduce the number of questions you receive by adding useful descriptions wherever possible.
Even after incorporating descriptions, you still might be asked about various aspects of your OpenAPI spec. At this point, you might be thinking, “Sharon, you deceived me! I added all those descriptions, yet the questions keep coming.”
Before you give up, have you thought about adding examples?
Let’s look at this parameter:
1 2 3 4 5 6 7 |
parameters: - name: id in: path required: true schema: type: string description: The user id. |
Based on the example, we understand that “id” is a string and serves as the user’s identifier. However, despite your QA team relying on your OpenAPI spec for their tests, they are encountering issues. They inform you that they are passing a string, yet the API call fails. “That’s because you’re not passing valid IDs,” you tell them. You rush to add an example to your OpenAPI spec:
1 2 3 4 5 6 7 8 |
parameters: - name: id in: path required: true schema: type: string example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b description: The user id. |
After you update your spec, a follow-up question arrives: Would “d0656a1f-1lac-4n7b-89de-3e8ic292b2e1” be a good example as well? The answer is no since the characters “l” and “n” in the example are not valid hexadecimal characters, making them illegal in the UUID (Universally Unique IDentifier) format:
1 2 3 4 5 6 7 8 9 |
parameters: - name: id in: path required: true schema: type: string format: uuid example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b description: The user id. |
Finally your QA team has all the information they need to interact with the endpoints that use this parameter.
But what if a parameter is not in a common format? That’s when regex patterns come in:
1 2 3 4 5 6 7 8 9 |
parameters: - name: id in: path required: true schema: type: string pattern: '[a-f0-9]{32}' example: 2675b703b9d4451f8d4861a3eee54449 description: A 32-character unique user ID. |
By using the pattern field, you can define custom validation rules for string properties, enabling more precise constraints on the data accepted by your API.
You can read more about formats, examples and patterns here.
This list of shortcomings is not exhaustive, but it contains the most common and easily fixable ones. By making these improvements, you are laying the foundation for successful API documentation. When working on your spec, put yourself in the shoes of a new API consumer, since this is their initial interaction with it. Ensure that it is well documented and easy to comprehend, and set the stage for a positive developer experience.