While the OpenAPI Specification tries to accommodate most use cases, additional data can be added to extend the specification at certain points.
OAS, v3.0
Introduction
This is the first in a (planned) multi-part series on OAS 3 - the OpenAPI 3.0 Specification. In this post, I’d like to introduce the reader to what I believe is one of OpenAPI’s most powerful features, Specification Extensions.
Prerequisites
This post assumes basic familiarity with OpenAPI 3 - specifically, it is assumed that the reader has at least a basic understanding of:
- paths
- operations
- schemas for:
(Need a refresher?)
These resources should get you up to speed!
What are Extensions?
Extensions are just extra data that you’re allowed to include in an OpenAPI spec which have no inherent semantic value. The specification allows API authors to attach specially formatted keys to specific object types, in order to provide consumers with additional metadata.
Here is an example of some extension data added to an Operation Object:
#...
paths:
/stuff
get:
operationId: getStuff
x-my-extension: >
The value here can actually be any valid JSON data that suits you!
I picked a string because it let me sneak some more
prose into a block of code. :)
content:
# ...
That might not seem extremely useful at first glance, but the implications for system design and integration are rather profound. Hang in a bit, and I’ll get you convinced, okay? Okay!
The Specifics
The field name MUST begin with
x-
, for example,x-internal-id
. The value can benull
, a primitive, an array or an object. Can have any valid JSON format value.
Extensions can be placed on almost every object type in the OAS 3 spec (see this page for a comprehensive list). Beyond that, there are no limitations placed on the extension data.
What are they for?
Extensions can be used to supplement the structural portrait of an API with metadata that can be used to interpret that structure.
The OAS spec provides a mechanism for API authors to define, unambiguously, what paths their APIs provide, what operations can be performed on those paths, authentication and authorization information for a variety of common auth schemes, API parameters and the rules for their serialization, and the structure of requests/responses in comprehensive detail. All of the information is human and machine readable and can be used to generate server stubs and full-featured API clients.
…So, what’s missing? …Meaning!
In essence, a spec defines the syntax of your API. Extensions allow you to define the semantics - providing API clients with guidance on how to leverage the structure of your API in the context of procedural- or business-logic.
“Oh, neat, Andrew, that sounds lofty and wonderful, but what does all that mean, and how can I use it to solve real-world problems!?“
Fair enough. First, let’s have a quick peek at some samples, then we’ll talk about what extensions are not. Afterwards, we’ll dig into an example or two to really hammer the utility of the thing home.
Okay? Okay!
Examples
- Mapping a response field to a display value (
x-display-name
) - Providing configuration information to authorization proxies (
x-google-allow
) - Providing production readiness info to developers (
x-preview
) - Handling a variety of pagination styles automatically (
x-pagination
)
What are they not?
OpenAPI extensions provide API consumers with extra data about an API, not about what’s in an API.
Note that in all of the examples above, the extensions don’t declare anything that is sent to or received from the API; instead, they provide context that is used to intepret things that are sent to or received from the API. Anything that is actually sent or received by an API and can be described by facilities in the spec, should be described by the spec!
You could, for example, add an x-my-content-type
annotation to a
ResponseObject
in order to indicate the content type returned, but this is a
bad use of specification extensions: HTTP already provides a way for us to
relay this information to a client (Content-Type
); The OpenAPI spec has
facilities to indicate to clients what range of content types an endpoint
produces and the format of the response on a content-type-by-content-type
basis.
A Simple Example
Let’s suppose you’ve got a number of API’s up and running, and they’re already
being used by a large number of clients in production. Let’s further suppose
that some new tool is being developed at your company, an inventory system. This
system is supposed to be able to list all of the entities in a given API. The
design requirements indicate that the UI should have just two fields for each
entity in a given API, Unique ID
and Type
.
You have OAS 3 specs for all of your API’s, so the Type
field is no problem:
the people building the inventory tool will just leverage the type name from
the components
section of your OAS 3 API description. But the Unique ID
bit
is a little more complicated: some of your API’s use an integer ID to uniquely
identify resources (an auto-incrementing field from the backing DB, perhaps),
some of your API’s use UUID’s, and some of them use URI’s. Worse, each of the
API’s uses a different name for their resource ID’s.
What to do?
There are a handful of approaches we could take here:
- ❌ Just ensure the documentation is up to date and punt the issue to the inventory team:
- Requires another team to maintain some mapping of API endpoints to unique ID fields.
- ❌ Change the API’s to have consistent ID types and names2:
- Requires all of your API maintainers and all of their clients to update their code.
- Breaks backwards compatibility without adding any new functionality.
- ✅ Relay that information via extension data and call it a day!
- No changes to the API, itself! (Just update the spec!)
- No changes to the payload format!
Comparing Approaches
Not an Extension
Here, the structure of MyResource
has changed. The API and clients will
require code changes to handle the new id
field. Documentation
will have to be written separately. The resources now have more than one
unique id:
schemas:
MyResource:
type: object
properties:
# All API's must now provide
# a UUID "id" field:
id:
type: string
format: uuid
# Backwards compatibility:
resourceId:
type: integer
format: int64
# ...
Notes
- The
id
field is in the response body.
Using An Extension
Here, MyResource
remains unchanged. The response object, is now annotated
with the extension x-unique-id
- which can be optionally leveraged by the
inventory tool team (and clients and doc generators!):
schemas:
MyResource:
type: object
# Indicate that the id field
# is unique without changing
# the payload. Doc generators
# can use the annotation too!
x-unique-id: resourceId
properties:
resourceId:
type: integer
format: int64
# ...
Notes
- The
x-unique-id
field is not in the response body.
Further Reading
If you got a kick out of this, I strongly encourage you to read:
- Morad Ankri’s Using extensions to support pagination in OpenAPI