Stoplight offers mock servers powered by OpenAPI so you can develop faster. Start mocking for free.
{{cta(‘d381534d-5cae-4e06-910b-84a0ffc8e807′,’justifycenter’)}}
Less is definitely more in software engineering. When we stand back and admire a job well done, the source of our pleasure is often because we’ve implemented a terse solution with the fewest lines of code possible. Thriftiness is definitely a virtue amongst software developers.
This tendency towards minimalism should also ring true in our OpenAPI descriptions (previously known as Swagger). A concise, well-structured specification document with a Components Object that includes appropriately reusable objects is the bare minimum an API designer should aim for. There are opportunities to manifest reuse both vertically – ensuring the overall length of the document is kept both digestible and manageable – and horizontally – by creating Schema Objects that can readily be used across different operations for the same endpoint.
To that end, API designers can structure their Schema objects so one base definition can readily support appropriate request or response bodies across POST, GET, PUT, and PATCH operations. This approach takes some thought on the part of the designer but is worth the effort to produce a more readily consumable specification (for humans and machines alike).
Creating and Extending Definitions
The first step for the API designer is to create a base object that encapsulates the properties of a given resource. By way of example, a designer may be creating a Customer API that implements a /customers
collection with each Customer resource identified by /customers/{CustomerId}
. The properties of the Customer resources are defined by the CustomerProperties
definition in our OpenAPI document:
components:
schemas:
CustomerProperties:
type: object
properties:
FirstName:
type: string
LastName:
type: string
DOB:
type: string
format: date-time
Segment:
type: string
enum:
- Young
- MiddleAged
- Old
- Moribund
Consumers of the Customer API can create a new customer by using the post
operation of the /customers
endpoint. However, certain properties are mandatory – FirstName
, LastName
and DOB
– so those constraints need to be enforced by combining the CustomerProperties
object with CustomerRequiredProperties
that defines a required
properties list:
components:
schemas:
CustomerRequiredProperties:
type: object
required:
- FirstName
- LastName
- DOB
The two definitions are combined using the allOf
keyword. These properties are encapsulated in the CreateCustomer
Request Body Object. The payload will need to be valid against both of the listed schemas for the implementation of the API to successfully process the request:
components:
requestBodies:
CreateCustomer:
description: Create a new customer
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/CustomerProperties'
- $ref: '#/components/schemas/CustomerRequiredProperties'
schemas:
Id:
type: integer
CustomerId:
type: object
properties:
Id:
$ref: '#/components/schemas/Id'
The Request Body Object can then be implemented in the post
operation of the /customers
endpoint and return the CustomerId
object to return an identifier for the newly created Customer resource. The CustomerId
definition can also be combined with the CustomerProperties
and CustomerRequiredProperties
definitions to create the Customer
definition, again using the allOf
keyword, to fully describe a Customer resource:
paths:
'/customers':
post:
tags:
- Customer
requestBody:
$ref: '#/components/requestBodies/CreateCustomer'
responses:
201:
description: Created
content:
application/json:
schema:
$ref: '#/components/schemas/CustomerId'
components:
schemas:
Customer:
allOf:
- $ref: '#/components/schemas/CustomerId'
- $ref: '#/components/schemas/CustomerProperties'
- $ref: '#/components/schemas/CustomerRequiredProperties'
The Customer API now has sufficient definitions to both create and describe a Customer resource. Taking this structured approach to create the definitions gives us the flexibility to reuse them across HTTP methods.
GET One or GET Many
The obvious first reuse of these objects is for a GET operation. When implementing a GET to retrieve a given Customer resource we can simply return CustomerProperties
as we already know the value of CustomerId
and don’t need it back in the response. However, the API consumer will also be interested in understanding the mandatory properties so the Response Object should also include the CustomerRequiredProperties
definition.
However, if the client wants to retrieve all the Customers that they are authorized for then the API can return an array of Customer
items. This definition includes the CustomerId
so we can easily identify a given Customer resource.
paths:
/customers:
get:
tags:
- Customer
responses:
200:
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Customer'
/customers/{CustomerId}:
get:
tags:
- Customer
parameters:
- $ref: '#/components/parameters/CustomerId'
responses:
200:
description: OK
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/CustomerProperties'
- $ref: '#/components/schemas/CustomerRequiredProperties'
components:
parameters:
CustomerId:
name: CustomerId
in: path
required: true
schema:
$ref: '#/components/schemas/Id'
By using these three schemas, we’ve managed to avoid unnecessary repetition in our OpenAPI description. We’ve kept the document readable while making it easy to extend to other endpoints and methods.
Update Some or All Properties
We can really see the power of reuse if we want to implement a PUT to update a given Customer resource. With the expectation that we are replacing the properties of a resource completely, we can just use reuse CreateCustomer
definition as is (perhaps renaming it to CreateUpdateCustomer
). This definition will apply the same constraints as when the Customer resource was created:
paths:
/customers/{CustomerId}:
put:
tags:
- Customer
requestBody:
$ref: '#/components/requestBodies/CreateCustomer'
parameters:
- $ref: '#/components/parameters/CustomerId'
responses:
204:
description: Updated
However, if we are implementing a PATCH – where we look to update only the properties that need to be changed – then we can simply reference the CustomerProperties
definition as the required
list should not be applied. However, to successfully implement a PATCH operation we need to allow clients to remove optional properties. One approach to accomplishing this is by implementing JSON Merge Patch where sending null
indicates that a property should be removed.
We can therefore combine the CustomerProperties
definition with the nullable
keyword using allOf
so that any optional fields can also be removed:
paths:
/customers/{CustomerId}:
patch:
tags:
- Customer
requestBody:
description: Update customer with properties to be changed
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/CustomerProperties'
- type: object
properties:
Segment:
nullable: true
parameters:
- $ref: '#/components/parameters/CustomerId'
responses:
204:
description: Updated
Our full OpenAPI document is at a minimum length while providing readable implementations of GET, POST, PUT, and PATCH.
Designing for Reuse
The examples above show how the OpenAPI specification can engender significant amounts of reuse from relatively few objects. By incorporating this design approach, it’s possible to create terse, well-constructed API specification documents that you as an API designer can stand back and be proud of.