Share generated openapi file between projects

I have a java spring backend and a js-react frontend.

The openapi-gradle-plugin starts the compiled spring application and generates an openapi document (OpenApiGeneratorTask)

The org.openapi.generator-plugin can generate a js library from this openapi document.

As described in the docs I’ve created a configuration and added an artifact in the backend project:

val openApi: Configuration by configurations.creating {
    isCanBeConsumed = true
    isCanBeResolved = false
}
afterEvaluate {
    val openApiGeneratorTask by tasks.named<OpenApiGeneratorTask>("generateOpenApiDocs")
    artifacts {
        // outputDir and outputFileName are properties
        add("openApi", openApiGeneratorTask.outputDir.file(openApiGeneratorTask.outputFileName)) {
            builtBy(openApiGeneratorTask)
        }
    }
}

The frontend consumes it using the following code:

openApiGenerate {
    inputSpec.set(provider { openApiConf.singleFile.path })
}

val openApiConf by configurations.creating {
    isCanBeConsumed = false
    isCanBeResolved = true
}

dependencies {
    openApiConf(project(mapOf(
        "path" to ":backend",
        "configuration" to "openApi")))
}

This only works, if I first build the backend project manually. Otherwise I get the following error:

A problem was found with the configuration of task ':frontend:openApiGenerate' (type 'GenerateTask').
  - In plugin 'org.openapi.generator' type 'org.openapitools.generator.gradle.plugin.tasks.GenerateTask' property 'inputSpec' specifies file 'D:\tmp\2021-12\gradle\proj\backend\build\openapi.json' which doesn't exist.

Maybe related is another problem / warning I get:

Execution optimizations have been disabled for task ':backend:jar' to ensure correctness due to the following reasons:
  - Gradle detected a problem with the following location: 'D:\tmp\2021-12\gradle\proj\backend\build\classes\java\main'. Reason: Task ':backend:jar' uses this output of task ':backend:generateOpenApiDocs' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed. Please refer to https://docs.gradle.org/7.2/userguide/validation_problems.html#implicit_dependency for more details about this problem.
  - Gradle detected a problem with the following location: 'D:\tmp\2021-12\gradle\proj\backend\build\libs\backend-plain.jar'. Reason: Task ':backend:forkedSpringBootRun' uses this output of task ':backend:jar' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed. Please refer to https://docs.gradle.org/7.2/userguide/validation_problems.html#implicit_dependency for more details about this problem.
  - Gradle detected a problem with the following location: 'D:\tmp\2021-12\gradle\proj\backend\build\resources\main'. Reason: Task ':backend:jar' uses this output of task ':backend:generateOpenApiDocs' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed. Please refer to https://docs.gradle.org/7.2/userguide/validation_problems.html#implicit_dependency for more details about this problem.
  - Gradle detected a problem with the following location: 'D:\tmp\2021-12\gradle\proj\backend\build\tmp\jar\MANIFEST.MF'. Reason: Task ':backend:jar' uses this output of task ':backend:generateOpenApiDocs' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed. Please refer to https://docs.gradle.org/7.2/userguide/validation_problems.html#implicit_dependency for more details about this problem.

I can of course manually set a dependencies between the tasks, but don’t understand why this is necessary.
Explicitly calling the generateOpenApiDocs task automatically builds the java code and generates the openapi document without any warnings.

Other questions:

  • The openapi-gradle-plugin creates the “generateOpenApiDocs” task in an afterEvaluate function: OpenApiGradlePlugin.kt
    Is my usage of afterEvaluate the correct way to add the artifact?
  • Do I have to convert the input configuration to a provider?
    inputSpec.set(provider { openApiConf.singleFile.path })

I am still looking for a solution to this problem.

If I delete the backend jars and the backend openapi.json file and try to build the frontend the following tasks are executed:

Here are the tasks, if I build the openapi.json file manually:
image

Apparently gradle realizes, that I am not looking for the jar and disables the compileJava task.
As I’ve added builtBy(openApiGeneratorTask) I was hoping that gradle would instead execute the openApiGeneratorTask.

bump

Do you have an idea how to find out, why this doesn’t work?

Should I open an issue?

I finally found out, why the document was not generated.

In my frontend I only referenced the configuration in the exension with:

openApiGenerate {
    inputSpec.set(provider { openApiConf.singleFile.path })
}

This however does not create a dependency on openApiConf configuration.

By adding the configuration to the inputs, gradle now (re)generates the spec if it doesn’t exist.

val openApiGenerateTask by tasks.named<GenerateTask>("openApiGenerate") {
    inputs.property("spec", openApiConf)
}

I am not sure if I should still use the openApiGenerate extension or if it would be good practice to move all configurations into the tasks.named… block.

The warnings about the explicit or implicit dependencys are still present. Any hints on how to get rid of them are still welcome.

I recently needed to do something similar, but in my case I was sharing compiled test classes with other subprojects that rerun the same tests with different dependency versions. It looks like what you don’t have correct is the configuration of the consumer so that it correctly depends on the production of the shared artifact(s):

In the producer subproject:

val sharedTestClasses: Configuration by configurations.creating {
    isCanBeConsumed = true
    isCanBeResolved = false
}

val testClasses: Task by tasks.getting

val sharedTestClassesJar by tasks.registering(Jar::class) {
    dependsOn(testClasses)
    archiveClassifier.set("tests")
    from(sourceSets.test.get().output)
}

artifacts {
    add(sharedTestClasses.name, sharedTestClassesJar)
}

In consumer subproject:

val externalTestClasses: Configuration by configurations.creating {
    isCanBeConsumed = false
    isCanBeResolved = true
}

dependencies {
    externalTestClasses(
        project(mapOf("path" to ":producer-project-name", "configuration" to "sharedTestClasses"))
    )
}

You will not have a Jar task, obviously, since your artifact(s) will be file(s), but I included the producer and consumer for completeness. For me, with Gradle 7.3, this correctly makes the consumer project depend on the producer task that creates the items to be shared.