Contents:
Extensions to OpenAPI¶
OpenAPI v3.0 provides only rudimentary support for defining links between documents. Specifically you can define a forward-pointing link which can extract values from the origin operation (e.g. from the request querystring, or response body) and pass them as request parameters to the destination operation.
For Apigraph we want to be able to completely specify the dependency relationship between API endpoints, across multiple interconnected APIs. Additionally, and importantly, we want to be able to take a leaf in the graph and trace back up its branches to the root request(s). In other words to extract all the prerequisites for any request.
For our use case we encountered three problems with OpenAPI 3.0 Links specification, which we have addressed by defining extension properties, as allowed by the OpenAPI spec. The issues that we ran into are:
There is no way to use extracted values in the destination operation’s request body, except as the whole
requestBody. For example you cannot take a value from the response body and use it as a field in the request body of the downstream request. This is due to limitations in the definition of request parameters - they can only address locations like the querystring or headers. We are not the first to notice this problem, there is an open issue here where we have proposed our extension format.The current forward-pointing link structure means that origin operations need to know of, and specify, all of their downstream dependents. Realistically that is only suited to the same-document use case. If you imagine the scenario of an organisation using microservices, where each service is managed by a different team, then you will find it inconvenient to define links in this way. It means you have to rely on a different team to specify your dependency requirement in the OpenAPI doc they are responsible for. In the case where your API has a dependency on a 3rd-party API then it becomes impossible. Instead we propose an alternative backward-pointing link specification (“backlinks”) that allows downstream services to fully specify their upstream dependencies.
There can be multiple paths through the graph, i.e. multiple links or backlinks pointing to any one Operation. We would like a way to distinguish these paths so that Apigraph can select a particular unique path through the graph. Since “path” already has a meaning in OpenAPI we will call these paths “chains” (as in links-in-a-chain). Below we propose extensions which allow links and require backlinks to specify a named chain-id which they belong to.
x-apigraph-requestBodyParameters¶
This is an extension to the Link Object.
Field Name |
Type |
Description |
|---|---|---|
x-apigraph-requestBodyParameters |
Map[{JSON Pointer}, {expression}] |
The keys are JSON Pointers identifying locations in the target request body. The values are runtime expressions to extract values from the source Operation. |
Example
paths:
'/':
get:
responses:
'200':
description: ok
links:
submitLink:
operationId: postSubmit
x-apigraph-requestBodyParameters:
/foo: $response.body#/foo
'/submit':
post:
operationId: postSubmit
Here we have an operation GET / and its 200 status response defines a forward-pointing link to the postSubmit operation id (POST /submit). The link itself has a name, submitLink. So far this is all basic OpenAPI 3.0 stuff.
Under the Link Object we have our extension field x-apigraph-requestBodyParameters, which here defines a single link parameter.
The key of the parameter /foo is a JSON Pointer which selects a single field location in the link target’s request body.
The value of the parameter $response.body#/foo is a runtime expression which selects a value from the link source, exactly as used elsewhere in the OpenAPI spec - the portion after the # is also a JSON Pointer, selecting the value /foo from the response body of the source request.
x-apigraph-backlinks (Operation)¶
This is an extension to the Operation Object.
Our aim is to fully specify all of the upstream dependencies of the current operation, i.e. its prerequisite requests.
Conceptually it is similar to the links field in a Response Object and the structure is deliberately similar.
Field Name |
Type |
Description |
|---|---|---|
x-apigraph-backlinks |
Map[ |
A mapping of names to Backlink objects. |
x-apigraph-backlinks (Components)¶
This is an extension to the Components Object.
This allows us to support defining backlinks on an Operation via a $ref to a common definition in Components, the same as you can for links.
Field Name |
Type |
Description |
|---|---|---|
x-apigraph-backlinks |
Map[ |
A mapping of names to Backlink objects. |
Backlink Object¶
OpenAPI 3.0, Links go from Response -> Operation (downstream).
Backlinks are the reverse, Operation -> Response (upstream).
So here we identify a specific Response in an upstream Operation and select values from that Response, for use as parameters in the backlink’s parent Operation.
We must recognise that there can be multiple upstream paths which can lead to the target Operation, which our backlinks are defined on. We shall call these paths “chains” (as in links-in-a-chain, since “path” already has a meaning in OpenAPI spec).
The links and backlinks in each chain will be unified by means of their chainId, an arbitrarily chosen string name. Links and backlinks which do not specify an explicit chain-id will have null as their chain-id. In Apigraph we call these “anonymous” links.
There may be multiple backlinked operations required by the current operation. We might imagine these as operations which could be made in parallel, where all of them are necessary prerequisites of the current request. In that case they MUST share the same chain-id. Otherwise, optional prerequisites should be given distinct chain-ids.
(see also: x-apigraph-chainId below)
NOTE: we only ever specify the immediate ancestors of the current request. Do not confuse these parallel prerequisites for “grandparent” operations (i.e. they are not serial prerequisites-of-prerequisites).
We then extract the necessary values from these prerequisite operations, for use when making a request to the backlink’s parent Operation.
Fixed Fields
Field Name |
Type |
Description |
|---|---|---|
chainId |
|
The chain-id to which this Backlink object belongs. If not present then the Backlink implicitly belongs to the |
responseRef |
|
A JSON Reference identifying a specific Response in the target Operation. One of |
operationRef |
|
A JSON Reference identifying a specific Operation. One of |
operationId |
|
Name identifying a specific Operation in the current document. One of |
response |
|
Name identifying to a specific response in the otherwise specified Operation. REQUIRED if either |
parameters |
Map[ |
A mapping of parameter names (from the backlink’s parent operation) to runtime expressions to extract a value from the upstream Response which is the target of this backlink. |
requestBodyParameters |
Map[{JSON Pointer}, {expression}] |
A mapping of JSON Pointers (identifying values in the backlink’s parent Operation’s request body) to runtime expressions to extract a value from the upstream Response which is the target of this backlink. |
requestBody |
{expression} |
A runtime expression to extract a value from the upstream Response it and use as the request body of the current Operation. |
description |
|
A description of the link. CommonMark syntax MAY be used for rich text representation. |
server |
A server object to be used by the target operation (the one this backlink is defined on). |
Most fields are similar to their counterparts in the Link Object.
responseRef provides the most concise way to refer to an upstream response. As for operationRef, the value is a JSON Reference, but the target should be a specific Response rather than an Operation. For example:
responseRef: '#/paths/~12.0~1users~1%7Busername%7D/get/responses/200'
Alternatively you may instead use either operationId or operationRef in conjunction with response, for example:
operationRef: '#/paths/~12.0~1users~1%7Busername%7D/get'
response: '200'
The chainId field serves the same purpose for backlinks as the x-apigraph-chainId extension field does for forward-pointing links. IMPORTANT NOTE: if there are multiple backlinks from the same Operation and having the same chainId (which will be null if not specified) then they are all considered required prerequisites to that Operation, when traversing that particular chain with Apigraph.
The requestBodyParameters field serves the same purpose for backlinks as the x-apigraph-requestBodyParameters extension field does for forward-pointing links.
The requestBody field serves the same purpose for backlinks as the existing one for Link Object.
description and server are also as per Link Object.
Complete Example
openapi: 3.0.0
info:
title: Backlinks Example
version: 1.0.0
paths:
/1.0/users/{username}:
get:
operationId: getUserByNamev1
parameters:
- name: username
in: path
required: true
schema:
type: string
responses:
'200':
description: The User
content:
application/json:
schema:
$ref: '#/components/schemas/user'
/2.0/users/{username}:
get:
operationId: getUserByName
parameters:
- name: username
in: path
required: true
schema:
type: string
responses:
'200':
description: The User
content:
application/json:
schema:
$ref: '#/components/schemas/user'
/repositories/{username}:
get:
operationId: getRepositoriesByOwner
parameters:
- name: username
in: path
required: true
schema:
type: string
x-apigraph-backlinks:
Get User by Username:
chainId: default
operationId: getUserByName
response: "200"
parameters:
# parameter name in the parent Operation: value selector
username: $response.body#/username
Get User by Username v1:
chainId: v1
operationId: getUserByNamev1
response: "200"
parameters:
username: $response.body#/username
responses:
'200':
description: repositories owned by the supplied user
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/repository'
components:
schemas:
user:
type: object
properties:
username:
type: string
uuid:
type: string
repository:
type: object
properties:
slug:
type: string
owner:
$ref: '#/components/schemas/user'
Here there are two chains; default and v1.
This highlights one use-case for named link chains - in a versioned API you will have redundant links to any un-versioned parts of the API (or to other APIs which are on a different versioning schedule).
In Apigraph we want to be able to say, for the GET /repositories/{username} operation, “give me all the prerequisite operations in the v1 chain for this endpoint”.
By default, “anonymous” links and backlinks (from the
nullchain) will also be included in any named chain. This allows the chain to traverse documents which have not been explicitly marked up with the Apigraph chainId extension. It also allows to use anonymous links where otherwise multiple identical links would need to be specified for each chainId.
x-apigraph-chainId¶
This is an extension to the Link Object.
For Apigraph’s purposes, if the Link does not have an x-apigraph-chainId field then it belongs to the null chain-id.
Fixed Fields
Field Name |
Type |
Description |
|---|---|---|
x-apigraph-chainId |
|
The chain-id to which this Link Object belongs. If not present then the Link implicitly belongs to the |
Example
paths:
'/':
get:
responses:
'200':
description: ok
links:
submitLink:
operationId: postSubmit
x-apigraph-chainId: default
'/submit':
post:
operationId: postSubmit
Link/Backlink “multiplicity”¶
This is a semantic, rather than syntactic, extension to OpenAPI.
Take this example:
openapi: 3.0.0
info:
title: Links Example
version: 1.0.0
paths:
/2.0/users/{username}:
get:
operationId: getUserByName
parameters:
- name: username
in: path
required: true
schema:
type: string
responses:
'200':
description: The User
content:
application/json:
schema:
$ref: '#/components/schemas/user'
links:
userRepositories:
operationId: getRepositoriesByOwner
description: Get list of repositories
parameters:
username: $response.body#/username
/2.0/repositories/{username}:
get:
operationId: getRepositoriesByOwner
parameters:
- name: username
in: path
required: true
schema:
type: string
responses:
'200':
description: repositories owned by the supplied user
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/repository'
components:
schemas:
user:
type: object
properties:
username:
type: string
uuid:
type: string
repository:
type: object
properties:
slug:
type: string
owner:
$ref: '#/components/schemas/user'
Here we link /2.0/users/{username} --> /2.0/repositories/{username} and we say that the $response.body#/username value should be extracted from the first request and used as the username parameter in the second operation.
The response.body and the operation’s parameters both have a schema, so there is a type-checking that can be performed here to ensure that the type of the source value is compatible with that of the target parameter. In this example both are values have string type and it would type-check successfully.
In Apigraph we would say this this link is singular rather than multiple.
It’s not clear whether OpenAPI mandates any specific behaviour from validators. We can imagine them either strictly type-checking based on the schemas or doing no type-checking at all - in that case a type mismatch could simply mean that the client is expected to coerce the extracted value to the type of the parameter before making a request.
In Apigraph we take the type-checking approach and will expect the types to match strictly, with the exception of “link multiplicity”, where we make the semantic interpretation described below.
Consider this example:
openapi: 3.0.0
info:
title: Links Example
version: 1.0.0
paths:
/2.0/users:
post:
operationId: createUser
requestBody:
content:
application/json:
schema:
type: object
required:
- username
- name
properties:
username:
type: string
name:
type: string
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/user'
/2.0/users/batch/{userIds}:
get:
operationId: getBatchUsersById
parameters:
- name: userIds
in: path
required: true
schema:
type: array
style: simple # serialize as csv
items:
type: integer
minItems: 1
maxItems: 255
x-apigraph-backlinks:
CreateUser:
operationId: createUser
response: "201"
parameters:
user_ids: $response.body#/id
responses:
'200':
description: The User
content:
application/json:
schema:
$ref: '#/components/schemas/user'
components:
schemas:
user:
type: object
properties:
id:
type: integer
username:
type: string
name:
type: string
repository:
type: object
properties:
slug:
type: string
owner:
$ref: '#/components/schemas/user'
Here we can see the /2.0/users/batch/{userids} endpoint has a url segment which should contain comma-separated integer ids. The operation has a backlink to the createUser > 201 response which tells the client to extract the id field value from the response and use it to satisfy the userIds parameter of the getBatchUsersById operation.
We can see the types in this example do not match - the $response.body#/id schema has type: integer while the userIds parameter has type: array, items: integer.
Apigraph will make a special case if you are extracting a scalar value, like a
stringorinteger, and using it to satisfy anarrayparameter of matchingitemstype.
For this example, Apigraph will understand the link or backlink as having multiplicity or, in other words, that the prerequisite request should be made multiple times and the values from each collated into an array for use in a single downstream parameter target.
Apigraph will respect the quantifiers of the target array schema, like minItems: 1 and maxItems: 255, when repeating the prerequisite requests, which can be made in parallel.