API patching
Table of contents
Introduction
OpenAPI is a very large and somewhat complex standard and it offers a lot of ways to model the same thing. This can lead
to a lot of different ways to describe the same API. Sometimes an API specification only contains the attributes while
putting the constraints into a description
property rather than using constraint attributes like maxLength
or
required
. Sometimes a specification may constrain the data type as number
when in fact it is always an integer
. Or
the specification may contain a construct that is not supported by the code generator but could be replaced with a
supported construct without changing the data that actually goes over the wire.
Since we usually get the specifications from a third party and have no control over how this third party chooses to write the specification, we need a way to patch the specification to make it work for our purposes. And we want to do this in a way that allows the third party to send us an updated specification without us having to reapply the patches.
Configuring patches
Quarkus Kotlin OpenAPI supports three types of patches that can be applied on an OpenAPI specification before the generator creates Kotlin code, which we’ll look at in the following sections.
Overlays
An overlay is simply a merge of two partial OpenAPI specifications. The generator starts with one specification and
merges the contents of one or more additional specifications into the first. The merge will be done fully recursively.
Overlays are best suited to quickly add or change existing properties. For example, let’s say we got an OpenAPI spec
which defines a User
object:
...
components:
schemas:
User:
type: object
properties:
id:
type: string
example: fbed0fb3-b5e4-4de2-b628-db4af24ba859
description: "A UUID with the user's ID."
pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
userName:
type: string
maxLength: 255
displayName:
type: string
maxLength: 255
required:
- userName
- displayName
...
The id
property is only set when the User
is retrieved, not when it is created. So ideally this property should be
marked readOnly
but the creator of the spec didn’t add this. However it would be really nice if we didn’t have to work
around this problem in our code. So we would like to patch a readOnly: true
to the id
property. With an overlay, we
can do this quickly. We create a new file server-openapi-overlay.yaml
and this file will only contain the parts we
want to add:
components:
schemas:
User:
properties:
id:
readOnly: true
Now we just need to tell the plugin to apply this overlay by adding it to the <sources>
section in the plugin’s
configuration in pom.xml
:
...
<executions>
<execution>
...
<configuration>
<sources>
<source>${project.basedir}/src/main/resources/server-openapi.yaml</source>
<!-- Apply an overlay on the openapi we got -->
<source>${project.basedir}/src/main/resources/server-openapi-overlay.yaml</source>
</sources>
</configuration>
...
</execution>
</executions>
...
The code generator will now merge both files into one and now the id
property is read-only. This way we can quickly
make minor changes and keep the original OpenAPI spec intact.
JSON patches
Overlays are nice for quickly adding or changing properties, but there are some things that cannot be done with them.
The most obvious limitation is that overlays only can add or change existing parts, but cannot delete a part. In the
previous example, the id
property is described to be a UUID and also has a regular expression verifying that.
id:
type: string
example: fbed0fb3-b5e4-4de2-b628-db4af24ba859
description: "A UUID with the user's ID."
pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
But actually OpenAPI has a built-in UUID format and our generator would support this. But the way this specification is
written the generator will map this ID to a kotlin.String
rather than a java.util.UUID
. So we would like to remove
the pattern
and instead add a format: uuid
entry. We can do this with a JSON patch:
# Remove RegEx pattern and add UUID format to the id property of the User schema
- op: remove
path: /components/schemas/User/properties/id/pattern
- op: add
path: /components/schemas/User/properties/id/format
value: uuid
We save this patch in a file server-openapi-jsonpatch.yaml
and add it to the plugin’s configuration in pom.xml
:
...
<execution>
...
<configuration>
<sources>
...
</sources>
<patches>
<!-- A a JSON-patch to the openapi we got -->
<patch>jsonpatch://${project.basedir}/src/main/resources/server-openapi-jsonpatch.yaml</patch>
</patches>
</configuration>
</execution>
...
All JSON patches will be applied in the order they are defined in the configuration and after the overlays.
JSONata patches
JSONata is a query and transformation language for JSON data. It enables us to do more complex transformations on the OpenAPI specification. Let’s say the creator of our specification was consistent and all ID fields are marked with a pattern. Patching all these ID fields with JSON patches will require a lot of patches and makes it easy to forget one. Instead with JSONata we can do this in one go:
/* Find all strings with a UUID pattern and replace it with format: uuid */
$ ~> | **[type='string' & pattern='^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$'] | {"format" : "uuid"},["pattern"] |
We can save this JSONata patch into a file named server-openapi-patch.jsonata
and add it to the plugin’s configuration
in pom.xml
:
...
<execution>
...
<configuration>
<sources>
...
</sources>
<patches>
<!-- A JSONata patch to the openapi we got -->
<patch>jsonata://${project.basedir}/src/main/resources/server-openapi-patch.jsonata</patch>
</patches>
</configuration>
...
</execution>
...
Like JSON patches, JSONata patches will also be applied after the overlays in the specified order. All patches can be mixed, so we can have overlays, JSON patches, and JSONata patches in the same configuration. This allows us to use a patch format that is most suitable for the change we want to make.
Previewing the patch result
When we patch a file, it is very useful to see what the result of all applied patches is. This can be done with a configuration setting in the plugin’s configuration in pom.xml
:
...
<execution>
...
<configuration>
<sources>
...
</sources>
<patches>
...
</patches>
<!-- Write the patched OpenAPI spec to a file, for debugging -->
<debugOutputFile>${project.build.directory}/debug.output.json</debugOutputFile>
</configuration>
...
</execution>
...
Now we can inspect the file target/debug.output.json
to see the result of all patches applied.