Enhancing OpenAPI Code Generation With Custom Features
Enhancing the out of the box generator to automatically retry (non-4xx) failed calls
In part one of this series, we showed how to use the open api code generation gradle plugin to create ready to use clients (and models) for REST services in a matter of seconds. This generated code can be used (without modification) to make REST calls to the endpoints declared in an open api specification. This generation process and it’s subsequent usage saves hours of time, and guarentees correctness.
While this “out of the box” generated code works and has many benefits, it can be advantageous to enhance the code that is generated with additional features. These modifications ensure that any project using the plugin (which should be every project that makes a REST call within your organization) will get code that follows best practices, without the overhead of writing even a single line of code. That is what we will do in this article: modify the code generation process to generate code that automatically retries failed calls (using exponential backoff and jitter), as well as giving our users the option to easily disable this feature on any RPC.
If you want to skip to the final product; here are the links
- The open api generator: you will need to build this before building the example project, which uses it
- The example project, which uses the custom generator; to use this run a ./gradlew clean build then look at the AssessmentsApi.java file and notice the “makeRequestWithRetries” — this is the new “customized” code
Create a project to use the code generation
- Create a new gradle project using gradle init (choose library option and leave the rest as defaults)
gradle init
- Copy infrastructure folder, the nested build.gradle, and open api generator ignore to your newly created project
- Run a gradle build — see that code is generated and that the project compiles/builds
./gradlew clean build
At this point, we have setup our project to use generate clients and models using the existing open api java client options (no customization yet) and ensured that the generated code compiles.
Your code should look like this right now
Modify the open api generator to create a new library option
- Clone the open api generator project (note: I forked it before I began working rather than just cloning it)
git clone https://github.com/OpenAPITools/openapi-generator.git
- In the open api generator project: Go to the JavaClientCodegen class and add new option * for “nativeWithRetries”to the supportedLibraries map (copy the same values as native, just give it a different name)
//at top of file
public static final String NATIVE_WITH_RETRIES = "nativeWithRetries";//...
//...supportedLibraries.put(NATIVE_WITH_RETRIES, "HTTP client: Java native HttpClient. JSON processing: Jackson 2.9.x. Only for Java11+"); //same values as native
- In the open api generator project: In the JavaClientCodegen class,find all the uses of NATIVE being used in an if statement, put an “or” for native with retries — this will ensure that all our library will generate all the same supporting files as the “native” choice (notice how I added the part in bold below). Find the changes I made here
if ((WEBCLIENT.equals(getLibrary()) && "threetenbp".equals(dateLibrary)) || NATIVE.equals(getLibrary()) || NATIVE_WITH_RETRIES.equals(getLibrary())) {
- In the open api generator project: run a maven build
./mvnw clean install -DskipTests
- In the client project: add plugin repositories configuration block to the top of the settings.gradle so that it checks for plugins we build locally
pluginManagement {
repositories {
mavenLocal()
gradlePluginPortal()
}
}
//your original settings.gradle contents should be here, beneath the pluginManagement block you should paste
- In the client project: in your build.gradle, adjust to use the new version of the plugin you just published
id "org.openapi.generator" version "5.4.0-SNAPSHOT"
At this point, we have registered a new library option to use with the java client, and we have verified it is listed as an option in our client application
You can see what the openapi project should look like now here
You can see what the client project should look like now here
Copy the existing native functionality to our new library option
- In the open api project: copy the entire “native” folder to “nativeWithRetries” (run the following command from root of repository)
cp -r modules/openapi-generator/src/main/resources/Java/libraries/native modules/openapi-generator/src/main/resources/Java/librlibraries/nativeWithRetries
- In the open api project: run a (maven) build
./mvnw clean install -DskipTests
- In the client project: change the library option in the openApiConfiguration block to the newly created library option “nativeWithRetries”
- In the client project: run a gradle build and verify things work
./gradlew clean build
At this point, we have created our own custom library option that we will modify in the later steps to customize the generated code. We are getting close!
You can see what the openapi project should look like now here
You can see what the client project should look like now here
Modify the mustache templates (which get transformed into generated code)
- In the open api project: replace the entire api.mustache file in the “nativeWithRetries” directory with this one
- In the open api project: run a (maven) build
./mvnw clean install -DskipTests
- In the client project: add a reslience4j-retry dependency to your build.gradle
implementation "io.github.resilience4j:resilience4j-retry:$resilience4jVersion"
- In the client project: run a gradle build— look at the generated code operations, they should all be wrapped in “makeRequestWithRetries”
./gradlew clean build
If you want more information on how code generation works in conjunction with mustache files, please comment and I can make more in depth articles
At this point, we have modified the mustache templates so that the generated code now automatically retries (non 4xx) failed requests. The only thing left to do is give users the ability to turn off this capability for exceptional use cases (as all RPCs should be designed to be idempotent)
You can see what the openapi project should look like now here
You can see what the client project should look like now here
Give the option to turn off retries (for a certain operation) if desired
In order to understand how this will work, we must look at vendor-extensions, which are “used to describe extra functionality that is not covered by the standard OpenAPI Specification”. If there is no extension present (the default) our generated code will retry all rpc calls that fail (except for those that get a 4xx response) with exponential backoff and jitter. However, if an operation is specified with the following line in the open api specification, we will NOT automatically retry.
x-dont-retry: true
In order to implement this, do the following:
- In the open api project: replace the content of the api.mustache in the nativeWithRetriesFolder with this the contents found here
- In the open api project: run a maven build
./mvnw clean install -DskipTests
- In the client project: run a gradle build and inspect the contents of the two different operations. Note that api calls will be retried, except for those specified with the custom extension
./gradlew clean build
At this point, we are done! Nice work.
You can see what the openapi project should look like now here
You can see what the client project should look like now here (no change from before)
Conclusion
In this article, we’ve seen that while the open api generator is functional without customization; there are many reasons to personalize the code that is generated from the specification, which will be used by teams across your organization. We then wrote our own customization to have the generated code retry failed calls (except for 4xx responses) using exponential backoff and jitter, which should automatically remediate transient failures for our consumers. We recognized that while all RPCs should be idempotent, sometimes there are operations that cannot be retried. For these operations, we gave our users a way to disable automatic retries using a single line in their open api specification.
Other customization ideas
- Automatically generate an oauth token for authentication
- Automatically propagate a correlation-id
- Automatically send a request id (same for all retries in a given sequence)
*in your company, I’d suggest naming the generator the name of your company (I.e. johnDeer), not after a specific feature like we did here; as you will likely add more than one feature