How to generate sources from multiple OpenAPI specifications with Gradle
This is a short note on configuring a Java Gradle project that uses the OpenAPI Generator plugin to generate client/server code from multiple specifications, preventing unnecessary code regeneration during project builds.
This is an issue I faced on a project in late 2024, and was reproducible at the time of writing this note, but no guarantees this is still relevant.
Problem
On a project that uses Gradle as a build system, there was an issue when all client/server code generated from OpenAPI specs was regenerated for every build. This is expected when doing a full rebuild (a gradle clean build
locally or in a CI pipeline), but why would you want that when you are just running your unit tests? This actually makes the process of writing unit tests not very pleasant.
I still don’t know if that projects’ lack of tests was the result of this, or just nobody bothered to fix this issue due to the lack of tests 🤔
The Cause
In the existing configuration of the project, each generator task used the same folder as a target – build/generated/openapi
. This worked fine for the API code since each spec was built to a different package and didn’t mix. However, besides the code, the plugin also generates a set of project files (like maven pom.xml, etc.).
The code didn’t mix due to different packages, but those files did, and each next task was overwriting project files from the previous.
Because of that, for every build Gradle considered that the generated code (those project files specifically) did not match what was generated before, and it’s right about time to go generate everything again.
Example
Let’s make a minimal reproduction of the issue first. Here’s the recipe.
- Take two OpenAPI specs to your liking to generate Server API interfaces.
Spec A:
openapi: 3.0.3
info:
title: Book Catalog API
description: API for managing book catalog information
version: 1.0.0
servers:
- url: /api/v1
paths:
/books:
get:
summary: Get all books
operationId: getAllBooks
...
post:
summary: Add a new book
operationId: addBook
...
/books/{bookId}:
get:
summary: Get book by ID
operationId: getBookById
...
put:
summary: Update book
operationId: updateBook
...
delete:
summary: Delete book
operationId: deleteBook
...
components:
schemas:
...
Spec B:
openapi: 3.0.3
info:
title: Book Inventory API
description: API for managing book inventory and stock
version: 1.0.0
servers:
- url: /api/v1
paths:
/inventory:
get:
summary: Get inventory status for all books
operationId: getInventory
...
/inventory/{bookId}:
get:
summary: Get inventory status for a specific book
operationId: getInventoryItem
...
put:
summary: Update inventory for a book
operationId: updateInventory
...
/inventory/{bookId}/stock:
post:
summary: Add stock for a book
operationId: addStock
...
/inventory/{bookId}/checkout:
post:
summary: Checkout a book (reduce stock)
operationId: checkoutBook
...
components:
schemas:
...
- Season them with a simple Gradle build
Build Config:
plugins
// ... generic java project configuration ...
// Generate server stubs for book catalog API
tasks.
// Generate server stubs for book inventory API
tasks.
// Make sure the generated sources are included in the source sets
sourceSets
// Make sure the code is generated before compiling
compileJava.dependsOn tasks.inventoryApi
compileJava.dependsOn tasks.catalogApi
- Run
gradle build
The full source of the initial example is available here – github. It has a bit more code to be a fully functional app.
With this configuration, each time we run gradle build
the OpenAPI generator tasks will execute, even if the specs didn’t change. Even if nothing has changed at all.
❯ gradle build
> Task :catalogApi
################################################################################
# Thanks for using OpenAPI Generator. #
# Please consider donation to help us maintain this project 🙏 #
# https://opencollective.com/openapi_generator/donate #
################################################################################
Successfully generated code to /***/build/generated/openapi
> Task :inventoryApi
################################################################################
# Thanks for using OpenAPI Generator. #
# Please consider donation to help us maintain this project 🙏 #
# https://opencollective.com/openapi_generator/donate #
################################################################################
Successfully generated code to /***/build/generated/openapi
> Task :compileJava
> Task :processResources UP-TO-DATE
> Task :classes
> Task :resolveMainClassName UP-TO-DATE
> Task :bootJar UP-TO-DATE
> Task :jar UP-TO-DATE
> Task :assemble UP-TO-DATE
> Task :compileTestJava NO-SOURCE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test NO-SOURCE
> Task :check UP-TO-DATE
> Task :build UP-TO-DATE
BUILD SUCCESSFUL in 1s
7 actionable tasks: 3 executed, 4 up-to-date
Let’s try to fix that.
The Crutch 🩼
A logical solution that comes to mind is to disable generation of those project files. Unfortunately, all I could find are issues like this one – [REQ] Option to generate only the models/controllers
Instead, we will do two things:
- exclude as much project files as possible from generation, and
- extract each API into its own folder.
Technically, you could only do the second, and it would solve the issue, but I wanted to keep the sources as clean as possible.
Cleanup 🧹
The only working way to remove unnecessary project files happens to be through .openapi-generator-ignore
. Put a file like this into the root (or wherever you like):
*
**/*
!**src/main/java/**/*
This file excludes from generation all files that are not in the source directory. Exactly what we needed.
But there is one issue still, which requires the second step. There are two metadata files added in .openapi-generator
folder: FILES
and VERSION
. They contain a list of all generated files, and version of the generator accordingly. And they are not excluded by the ignore file :(

So now when several tasks generate sources – each will overwrite those files from the previous.
Let’s continue.
Extraction
The only option we have left is to put every API interface into its own folder.
We will change the task configuration a bit, and will write some more config to attach the sources to the sourceSets
.
Significant changes are highlighted.
// Generate server stubs for book catalog API
tasks.
//rest of the code ...
// Disable the default openApiGenerate task as we use our specific tasks
tasks.
// Configure our specific OpenAPI generator tasks to be dependencies of compileJava
tasks..all
Another benefit of this approach is it’s automated, so you don’t have to add the dependencies for every new generator task by hand.
Note we had to disable the default task from the plugin, so it wouldn’t mix with our custom generator tasks. Otherwise the build wouldn’t work.
Result
This is what we get for the first clean build:
❯ gradle build
> Task :catalogApi
################################################################################
# Thanks for using OpenAPI Generator. #
# Please consider donation to help us maintain this project 🙏 #
# https://opencollective.com/openapi_generator/donate #
################################################################################
Successfully generated code to /Users/andts/Personal/openapi-gen-post/build/generated/openapi/catalogApi
> Task :inventoryApi
################################################################################
# Thanks for using OpenAPI Generator. #
# Please consider donation to help us maintain this project 🙏 #
# https://opencollective.com/openapi_generator/donate #
################################################################################
Successfully generated code to /Users/andts/Personal/openapi-gen-post/build/generated/openapi/inventoryApi
> Task :openApiGenerate SKIPPED
> Task :compileJava
> Task :processResources
> Task :classes
> Task :resolveMainClassName
> Task :bootJar
> Task :jar
> Task :assemble
> Task :compileTestJava NO-SOURCE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test NO-SOURCE
> Task :check UP-TO-DATE
> Task :build
BUILD SUCCESSFUL in 1s
7 actionable tasks: 7 executed
And this is the process when only the application code has changed, but not the specs:
❯ gradle build
> Task :catalogApi UP-TO-DATE
> Task :inventoryApi UP-TO-DATE
> Task :openApiGenerate SKIPPED
> Task :compileJava
> Task :processResources UP-TO-DATE
> Task :classes
> Task :resolveMainClassName
> Task :bootJar
> Task :jar
> Task :assemble
> Task :compileTestJava NO-SOURCE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test NO-SOURCE
> Task :check UP-TO-DATE
> Task :build
BUILD SUCCESSFUL in 1s
7 actionable tasks: 4 executed, 3 up-to-date
As you can see, Gradle will correctly detect that the generated code is UP-TO-DATE
and will skip the tasks.
Consider this approach if you encounter similar issues in your projects.
This solution is particularly useful if you:
- build with Gradle
- several OpenAPI specs
- you build often
The example code is available here: github. You can see the changes for the steps of this post in the commit history.