Enhancing OpenAPI Code Generation With Custom Features

Connor Butch
7 min readJan 7, 2022

--

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
gradle init will set you up with a gradle project
./gradlew clean build
After running the gradle command, you should see the generated models and clients in the lib/build/generated directory

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
Register the native with retries as an option
  • 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())) {
Some of the changes made in this commit are shown above
  • 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
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
You should now see the “nativeWithRetries” folder containing a bunch of files (at this point, it’s an exact copy of the native folder)
  • In the open api project: run a (maven) build
./mvnw clean install -DskipTests
change the library option to be “nativeWithRetries” (it had previously been just native)
  • 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
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
notice how the request is now wrapped with “makeRequestWithRetries”

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
The createAssessment operation is not idempotent, and hence is annotated with x-dont-retry

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
This only change here fancy is an if statement— if operation has the extension, don’t retry it. Otherwise, wrap with retries
  • 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
Notice how now the operation with the don’t retry does NOT have the call wrapped in retries (shown on the left), while all other calls do (shown on the right)

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

--

--

Connor Butch

I write about coding, AWS mainly. Outside of that, I enjoy traveling, cooking, dogs, and meeting new people.