I'm trying to use OpenAPI to generate JAX-RS inter...
# community-support
m
I'm trying to use OpenAPI to generate JAX-RS interfaces from
src/main/resources/api.yml
and then build the generated interfaces. I've got it working, but I just want to check that I'm not doing anything here that violates Gradle best practices. Here is `build.gradle.kts`:
Copy code
plugins {
    `java-library`
    id("org.openapi.generator") version "7.10.0"
}

// Create a task to validate the OpenAPI YAML.
openApiValidate {
    inputSpec = "$projectDir/src/main/resources/api.yaml"
}

// Check that the Open API YAML is valid.
tasks.named("check") {
    dependsOn("openApiValidate")
}

// Create a task to generate the JAX-RS API from the OpenAPI YAML.
openApiGenerate {
    generatorName = "jaxrs-spec"
    inputSpec = "$projectDir/src/main/resources/api.yaml"
    outputDir = layout.buildDirectory.dir("generated/sources/openApi").get().asFile.absolutePath

    // Generate only the apis and models. Don't generate the invokers or other supporting files.
    apiPackage = "org.example.api"
    modelPackage = "org.example.api"
    globalProperties = mapOf(
        "apis" to "",
        "models" to "",
    )

    // Generate an async interface for the apis. Use builders for the models.
    configOptions = mapOf(
        "generateBuilders" to "true",
        "hideGenerationTimestamp" to "true",
        "interfaceOnly" to "true",
        "sourceFolder" to "java/main",
        "supportAsync" to "true",
        "useJakartaEe" to "true",
        "useSwaggerAnnotations" to "false",
        "useTags" to "true",
    )
}

// Compile the JAX-RS API that is generated from the OpenAPI YAML.
tasks.named("compileJava") {
    dependsOn("openApiGenerate")
}

sourceSets {
    main {
        java {
            srcDir(layout.buildDirectory.dir("generated/sources/openApi/java/main"))
        }
    }
}

repositories {
    mavenCentral()
}

dependencies {
    api(libs.jackson.annotations)
    api(libs.jakartaAnnotation.api)
    api(libs.jakartaValidation.api)
    api(libs.jaxRs.api)
}
Only two small things I've noticed: • I know
$buildDir
is deprecated, but is there anything a bit more...elegant than
layout.buildDirectory.dir("generated/sources/openApi").get().asFile.absolutePath
? Compare that to
"$projectDir/src/main/resources/api.yaml"
. • In IntelliJ, it says that
build/generated/sources/openApi/java/main
is a "sources root", whereas
build/generated/sources/annotationProcessor/java/main
(for projects that use an annotation processor) is a "generated sources root". Is there anything I should do differently when adding a
srcDir
for generated sources? (But this is minor in the grand scheme of things.)
v
From a cursory look there are two bad things left. 1. do not configure a directory that is a task output manually as a task input or source directory 2. do not use explicit
dependsOn
except with a lifecycle task on the left-hand side Actually both problems are related. You add the explicit dependency because you did not properly wire task outputs to task inputs. But you only do it partly - always. What about other tasks that need source files besides
compileJava
, for example
sourcesJar
or any static code analyzer task that needs all sources, and so on. Any explicit
dependsOn
without lifecycle task on the left-hand side is a code smell and a sign for doing something not properly, most often not wiring task ouputs to task inputs properly like here. If the
openaApiGenerate
task properly declares its output files which are the sources files, then directly use that task as
srcDir
. That way you get all the output files as source files and the task dependency where necessary automatically. And maybe some nits: • like using
layout.buildDirectory
I'd also use
layout.projectDirectory
is there anything a bit more...elegant
Sure, make the maintainer of that task or extension adopt the new Gradle ways, so that it is not a
File
or `String`but a
RegularFileProperty
, then what you set it to is
layout.buildDirectory.dir("generated/sources/openApi")
. If they are not following best practices, you have to pay with a bit uglier usage.
Is there anything I should do differently
You can apply the
idea
plugin and add the directory as generated source dir, then it is marked appropriately in IJ.
Copy code
idea {
    module {
        generatedSourceDirs.add(...)
    }
}
m
Thanks! • For
openApiGenerate
, the
srcDir
should depend on that task now. OpenAPI's Github documentation for the Gradle plugin suggests using
dependsOn
to build the generated sources, which is what led me down the wrong path. • For
openApiValidate
, is using
dependsOn
OK there to add the
openApiValidate
task as a dependency of the
check
task? • It looks like there's an existing issue about getting
inputSpec
and
outputDir
to accept a file; I've added a comment to that. Hopefully this looks right (it also uses
layout.projectDirectory()
). Since everything still works with these changes, I assume that
openApiGenerate
is declaring its outputs properly behind-the-scenes:
Copy code
plugins {
    `java-library`
    id("org.openapi.generator") version "7.10.0"
}

// Create a task to validate the OpenAPI YAML.
openApiValidate {
    inputSpec = layout.projectDirectory.file("src/main/resources/api.yaml").asFile.absolutePath
}

// Check that the Open API YAML is valid.
tasks.named("check") {
    dependsOn("openApiValidate")
}

// Create a task to generate the JAX-RS API from the OpenAPI YAML.
openApiGenerate {
    generatorName = "jaxrs-spec"
    inputSpec = layout.projectDirectory.file("src/main/resources/api.yaml").asFile.absolutePath
    outputDir = layout.buildDirectory.dir("generated/sources/openApi").get().asFile.absolutePath

    // Generate only the apis and models. Don't generate the invokers or other supporting files.
    apiPackage = "org.example.api"
    modelPackage = "org.example.api"
    globalProperties = mapOf(
        "apis" to "",
        "models" to "",
    )

    // Generate an async interface for the apis. Use builders for the models.
    configOptions = mapOf(
        "generateBuilders" to "true",
        "hideGenerationTimestamp" to "true",
        "interfaceOnly" to "true",
        "sourceFolder" to "java/main",
        "supportAsync" to "true",
        "useJakartaEe" to "true",
        "useSwaggerAnnotations" to "false",
        "useTags" to "true",
    )
}

// Compile the JAX-RS API that is generated from the OpenAPI YAML.
sourceSets {
    main {
        java {
            srcDir(tasks.openApiGenerate)
        }
    }
}

repositories {
    mavenCentral()
}

dependencies {
    api(libs.jackson.annotations)
    api(libs.jakartaAnnotation.api)
    api(libs.jakartaValidation.api)
    api(libs.jaxRs.api)
}
v
OpenAPI's Github documentation for the Gradle plugin suggests using
dependsOn
to build the generated sources
Well, the internet is full of bad practice advices. 😞
• For
openApiValidate
, is using
dependsOn
OK there to add the
openApiValidate
task as a dependency of the
check
task?
check
is a lifecycle task. It does not have actions, its purpose is to depend on other tasks as kind of "phase". So yes, as I said, if the left-hand side is a lifecycle task like
check
, an explicit
dependsOn
is fine. At least if the intention is exactly like here, "if
check
is invoked, also do this checker task".
classes
is also a lifecycle task, but making
classes
depend on the generation task would not be good as the intention is a different one.
• It looks like there's an existing issue about getting
inputSpec
and
outputDir
to accept a file; I've added a comment to that.
Actually, as it is a
Property<String>
not
String
, you should not call
get()
. Calling
get()
at configuration time always introduces race conditions. If you for example
get()
the build directory and then later change the build directory to something else you will have the wrong value used. It should be something like
outputDir = layout.buildDirectory.dir("generated/sources/openApi").map { it.asFile.absolutePath }
.
m
Thanks! ACK on
get
vs.
map
. I've updated the code: • I've now shoved all this logic into a
buildSrc
plugin. • I no longer add the validate task as a dep of the
check
task, since the generate tasks also do validation;
check
already runs
compileJava
and the generate tasks (as transitive deps of
test
). • I'm now generating server and client code in two generate tasks. I gave each one a separate directory in `build/generated`; I vaguely recall that two tasks shouldn't share an output dir.
Copy code
import org.gradle.accessors.dm.LibrariesForLibs

plugins {
    `java-library`
    id("org.openapi.generator")
}

// Create the extension and some path utilities.
interface OpenApiJavaExtension {
    val packageName: Property<String>
    val inputSpec: RegularFileProperty
    val inputSpecPath: Provider<String>
        get() = inputSpec.map { it.asFile.absolutePath }
}

val extension = extensions.create<OpenApiJavaExtension>("openApiJava")
extension.packageName.convention("")
extension.inputSpec.convention(layout.projectDirectory.file("src/main/resources/api.yaml"))

afterEvaluate {
    if (extension.packageName.get().isEmpty()) {
        throw GradleException("openApiJava.packageName must be set")
    }
}

fun buildDirPath(relativePath: String): Provider<String> =
    layout.buildDirectory.dir(relativePath).map { it.asFile.absolutePath }

// Create a task to validate the OpenAPI YAML.
tasks.register<org.openapitools.generator.gradle.plugin.tasks.ValidateTask>("openApiJavaValidate") {
    group = "OpenAPI Java"
    description = "Validates the OpenAPI YAML."

    inputSpec = extension.inputSpecPath
}

// Create a task to generate JAX-RS server interfaces from the Open API YAML.
val commonGenerateConfigOptions = mapOf(
    "dateLibrary" to "java8",
    "generateBuilders" to "true",
    "hideGenerationTimestamp" to "true",
    "openApiNullable" to "false",
    "sourceFolder" to "java/main",
    "useJakartaEe" to "true",
)

tasks.register<org.openapitools.generator.gradle.plugin.tasks.GenerateTask>("openApiJavaGenerateServer") {
    group = "OpenAPI Java"
    description = "Generates JAX-RS server interfaces from the OpenAPI YAML."
    logging.captureStandardOutput(<http://LogLevel.INFO|LogLevel.INFO>)

    generatorName = "jaxrs-spec"
    inputSpec = extension.inputSpecPath
    outputDir = buildDirPath("generated/sources/openApiServer")

    // Generate only the apis and models. Don't generate the invokers or other supporting files.
    val apiPackageName = extension.packageName.map { "$it.api" }
    apiPackage = apiPackageName
    modelPackage = apiPackageName
    globalProperties = mapOf(
        "apis" to "",
        "models" to "",
    )

    // Generate async interfaces for the apis.
    configOptions = commonGenerateConfigOptions + mapOf(
        "interfaceOnly" to "true",
        "supportAsync" to "true",
        "useSwaggerAnnotations" to "false",
        "useTags" to "true",
    )
}

// Create a task to generate Retrofit clients from the Open API YAML.
tasks.register<org.openapitools.generator.gradle.plugin.tasks.GenerateTask>("openApiJavaGenerateClient") {
    group = "OpenAPI Java"
    description = "Generates Retrofit clients from the OpenAPI YAML."
    logging.captureStandardOutput(<http://LogLevel.INFO|LogLevel.INFO>)

    generatorName = "java"
    inputSpec = extension.inputSpecPath
    outputDir = buildDirPath("generated/sources/openApiClient")

    // Generate everything for main. Don't generate test.
    val clientPackageName = extension.packageName.map { "$it.client" }
    apiPackage = clientPackageName
    modelPackage = clientPackageName
    invokerPackage = clientPackageName.map { "$it.retrofit" }
    globalProperties = mapOf(
        "apiTests" to "false",
        "modelTests" to "false",
    )

    // Generate Retrofit clients using Jackson.
    configOptions = commonGenerateConfigOptions + mapOf(
        "library" to "retrofit2",
        "serializationLibrary" to "jackson",
    )
}

// Compile the Java sources that are generated from the OpenAPI YAML.
sourceSets {
    main {
        java {
            srcDir(tasks.named("openApiJavaGenerateServer"))
            srcDir(tasks.named("openApiJavaGenerateClient"))
        }
    }
}

repositories {
    mavenCentral()
}

val libs = the<LibrariesForLibs>() // version catalog workaround

dependencies {
    api(libs.apacheOauth.client)
    api(libs.jackson.annotations)
    api(libs.jackson.databind)
    api(libs.jakartaAnnotation.api)
    api(libs.jakartaValidation.api)
    api(libs.jaxRs.api)
    api(libs.okhttp.okhttp)
    api(libs.retrofit.retrofit)
    implementation(libs.jackson.datatypeJsr310)
    implementation(libs.retrofit.converterJackson)
    implementation(libs.retrofit.converterScalars)
}
The only thing I can't figure is how to disable the Java compilation notes (notes, not warnings):
Copy code
> Task :api:compileJava
Note: /home/mike/IdeaProjects/sandbox-dropwizard/api/build/generated/sources/openApiClient/java/main/org/example/client/retrofit/auth/OAuthOkHttpClient.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
I thought that adding
options.isDeprecation = false
to the tasks of type
JavaCompile
would fix it, but apparently not.
v
I vaguely recall that two tasks shouldn't share an output dir.
Yes, never, or bad things happen. You should know this by heart, not only vaguely. 😄
I thought that adding
options.isDeprecation = false
to the tasks of type
JavaCompile
would fix it, but apparently not.
No,
options.isDeprecation = true
is like using
-Xlint:deprecation
.
false
is the default. If you want to suppress those notes, you need to check whether the compiler supports turning them off and then giving it the necessary argument. I don't think this is built-in as option in Gradle.
m
Yeah, I've done some digging, and I can't find anything about a compiler arg to suppress these notes. So, assuming that's true, either you allow the notes to be displayed, or you resort to this inelegant solution:
Copy code
tasks.withType<JavaCompile> {
    logging.captureStandardError(<http://LogLevel.INFO|LogLevel.INFO>) // suppress compilation notes
}
I double-checked this by removing an
api
dep to introduce compilation errors, but it does seem like suppressing INFO logs won't remove anything that you couldn't already learn from WARN and ERROR logs.