From 2f9cdeec436f4914824dc11377a78a19de7e482d Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Mon, 1 Jun 2026 13:24:19 -0700 Subject: [PATCH 1/4] Re-add jackson 3 support --- graphql-dgs-codegen-core/build.gradle | 1 + .../netflix/graphql/dgs/codegen/CodeGen.kt | 6 + .../generators/kotlin/KotlinPoetUtils.kt | 51 ++++-- .../kotlin2/GenerateKotlin2DataTypes.kt | 12 +- .../codegen/JacksonVersionDetectionTest.kt | 157 ++++++++++++++++++ .../dgs/codegen/gradle/GenerateJavaTask.kt | 12 ++ .../codegen/gradle/JacksonVersionDetector.kt | 49 ++++++ 7 files changed, 267 insertions(+), 21 deletions(-) create mode 100644 graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/JacksonVersionDetectionTest.kt create mode 100644 graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/JacksonVersionDetector.kt diff --git a/graphql-dgs-codegen-core/build.gradle b/graphql-dgs-codegen-core/build.gradle index 2792da501..f9d38a5e9 100644 --- a/graphql-dgs-codegen-core/build.gradle +++ b/graphql-dgs-codegen-core/build.gradle @@ -43,6 +43,7 @@ dependencies { testImplementation 'org.jetbrains.kotlin:kotlin-compiler' integTestImplementation 'com.fasterxml.jackson.module:jackson-module-kotlin' + integTestImplementation 'tools.jackson.core:jackson-databind:latest.release' } application { diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt index 29e5739e0..3d72494c9 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt @@ -584,6 +584,7 @@ class CodeGenConfig( var addDeprecatedAnnotation: Boolean = false, var trackInputFieldSet: Boolean = false, var generateJSpecifyAnnotations: Boolean = false, + var jacksonVersions: Set = emptySet(), ) { val packageNameClient: String = "$packageName.$subPackageNameClient" @@ -620,6 +621,11 @@ enum class Language { KOTLIN, } +enum class JacksonVersion { + JACKSON_2, + JACKSON_3, +} + data class CodeGenResult( val javaDataTypes: List = listOf(), val javaInterfaces: List = listOf(), diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinPoetUtils.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinPoetUtils.kt index febec82bb..ee231bf9b 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinPoetUtils.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinPoetUtils.kt @@ -22,10 +22,9 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo -import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder import com.netflix.graphql.dgs.codegen.CodeGen import com.netflix.graphql.dgs.codegen.CodeGenConfig +import com.netflix.graphql.dgs.codegen.JacksonVersion import com.netflix.graphql.dgs.codegen.generators.shared.CodeGeneratorUtils.capitalized import com.netflix.graphql.dgs.codegen.generators.shared.JAKARTA_GENERATED_ANNOTATION import com.netflix.graphql.dgs.codegen.generators.shared.PackageParserUtil @@ -128,31 +127,57 @@ fun jsonSubTypesAnnotation(subTypes: Collection): AnnotationSpec { } /** - * Generate a [JsonDeserialize] annotation for the builder class. + * Returns the configured Jackson versions, defaulting to Jackson 2 when none are configured + * (for backwards compatibility — see [JacksonVersion]). + */ +private fun jacksonVersionsOrDefault(config: CodeGenConfig): Set = + config.jacksonVersions.ifEmpty { setOf(JacksonVersion.JACKSON_2) } + +private fun jsonDeserializeClassName(version: JacksonVersion): ClassName = + when (version) { + JacksonVersion.JACKSON_2 -> ClassName("com.fasterxml.jackson.databind.annotation", "JsonDeserialize") + JacksonVersion.JACKSON_3 -> ClassName("tools.jackson.databind.annotation", "JsonDeserialize") + } + +private fun jsonPOJOBuilderClassName(version: JacksonVersion): ClassName = + when (version) { + JacksonVersion.JACKSON_2 -> ClassName("com.fasterxml.jackson.databind.annotation", "JsonPOJOBuilder") + JacksonVersion.JACKSON_3 -> ClassName("tools.jackson.databind.annotation", "JsonPOJOBuilder") + } + +/** + * Generate `@JsonDeserialize` annotations for the builder class, one per configured Jackson version. + * Jackson 2 lives in `com.fasterxml.jackson.databind.annotation`; Jackson 3 in `tools.jackson.databind.annotation`. * * Example generated annotation: * ``` * @JsonDeserialize(builder = Movie.Builder::class) * ``` */ -fun jsonDeserializeAnnotation(builderType: ClassName): AnnotationSpec = - AnnotationSpec - .builder(JsonDeserialize::class) - .addMember("builder = %T::class", builderType) - .build() +fun jsonDeserializeAnnotations( + config: CodeGenConfig, + builderType: ClassName, +): List = + jacksonVersionsOrDefault(config).map { version -> + AnnotationSpec + .builder(jsonDeserializeClassName(version)) + .addMember("builder = %T::class", builderType) + .build() + } /** - * Generate a [JsonPOJOBuilder] annotation for the builder class. + * Generate `@JsonPOJOBuilder` annotations for the builder class, one per configured Jackson version. + * Jackson 2 lives in `com.fasterxml.jackson.databind.annotation`; Jackson 3 in `tools.jackson.databind.annotation`. * * Example generated annotation: * ``` * @JsonPOJOBuilder * ``` */ -fun jsonBuilderAnnotation(): AnnotationSpec = - AnnotationSpec - .builder(JsonPOJOBuilder::class) - .build() +fun jsonBuilderAnnotations(config: CodeGenConfig): List = + jacksonVersionsOrDefault(config).map { version -> + AnnotationSpec.builder(jsonPOJOBuilderClassName(version)).build() + } /** * Generate a [JvmName] annotation for a kotlin property. diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2DataTypes.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2DataTypes.kt index c7adcf46a..9df488f1a 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2DataTypes.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2DataTypes.kt @@ -24,8 +24,8 @@ import com.netflix.graphql.dgs.codegen.generators.kotlin.ReservedKeywordFilter import com.netflix.graphql.dgs.codegen.generators.kotlin.addControlFlow import com.netflix.graphql.dgs.codegen.generators.kotlin.addOptionalGeneratedAnnotation import com.netflix.graphql.dgs.codegen.generators.kotlin.disableJsonTypeInfoAnnotation -import com.netflix.graphql.dgs.codegen.generators.kotlin.jsonBuilderAnnotation -import com.netflix.graphql.dgs.codegen.generators.kotlin.jsonDeserializeAnnotation +import com.netflix.graphql.dgs.codegen.generators.kotlin.jsonBuilderAnnotations +import com.netflix.graphql.dgs.codegen.generators.kotlin.jsonDeserializeAnnotations import com.netflix.graphql.dgs.codegen.generators.kotlin.jsonIgnorePropertiesAnnotation import com.netflix.graphql.dgs.codegen.generators.kotlin.jsonPropertyAnnotation import com.netflix.graphql.dgs.codegen.generators.kotlin.jvmNameAnnotation @@ -134,7 +134,7 @@ fun generateKotlin2DataTypes( TypeSpec .classBuilder("Builder") .addOptionalGeneratedAnnotation(config) - .addAnnotation(jsonBuilderAnnotation()) + .apply { jsonBuilderAnnotations(config).forEach { addAnnotation(it) } } .addAnnotation(jsonIgnorePropertiesAnnotation("__typename")) // add a backing property for each field .addProperties( @@ -193,11 +193,7 @@ fun generateKotlin2DataTypes( } // add jackson annotations .addAnnotation(disableJsonTypeInfoAnnotation()) - .addAnnotation( - jsonDeserializeAnnotation( - builderClassName, - ), - ) + .apply { jsonDeserializeAnnotations(config, builderClassName).forEach { addAnnotation(it) } } // add nested classes .addType(companionObject) .addType(builder) diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/JacksonVersionDetectionTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/JacksonVersionDetectionTest.kt new file mode 100644 index 000000000..6727584f2 --- /dev/null +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/JacksonVersionDetectionTest.kt @@ -0,0 +1,157 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.graphql.dgs.codegen + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class JacksonVersionDetectionTest { + private val schema = + """ + type Query { + movies: [Movie] + } + + type Movie { + title: String + director: String + } + """.trimIndent() + + @Test + fun `generates only Jackson 2 JsonDeserialize and JsonPOJOBuilder annotations when Jackson 2 is configured`() { + val result = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = "com.test", + language = Language.KOTLIN, + generateKotlinNullableClasses = true, + jacksonVersions = setOf(JacksonVersion.JACKSON_2), + ), + ).generate() + + val movieType = result.kotlinDataTypes.first { it.name == "Movie" } + val fileContent = movieType.toString() + + assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonDeserialize") + assertThat(fileContent).doesNotContain("tools.jackson.databind.`annotation`.JsonDeserialize") + + assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonPOJOBuilder") + assertThat(fileContent).doesNotContain("tools.jackson.databind.`annotation`.JsonPOJOBuilder") + } + + @Test + fun `generates only Jackson 3 JsonDeserialize and JsonPOJOBuilder annotations when Jackson 3 is configured`() { + val result = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = "com.test", + language = Language.KOTLIN, + generateKotlinNullableClasses = true, + jacksonVersions = setOf(JacksonVersion.JACKSON_3), + ), + ).generate() + + val movieType = result.kotlinDataTypes.first { it.name == "Movie" } + val fileContent = movieType.toString() + + assertThat(fileContent).contains("tools.jackson.databind.`annotation`.JsonDeserialize") + assertThat(fileContent).doesNotContain("com.fasterxml.jackson.databind.`annotation`.JsonDeserialize") + + assertThat(fileContent).contains("tools.jackson.databind.`annotation`.JsonPOJOBuilder") + assertThat(fileContent).doesNotContain("com.fasterxml.jackson.databind.`annotation`.JsonPOJOBuilder") + } + + @Test + fun `generates both Jackson 2 and 3 JsonDeserialize and JsonPOJOBuilder annotations when both are configured`() { + val result = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = "com.test", + language = Language.KOTLIN, + generateKotlinNullableClasses = true, + jacksonVersions = setOf(JacksonVersion.JACKSON_2, JacksonVersion.JACKSON_3), + ), + ).generate() + + val movieType = result.kotlinDataTypes.first { it.name == "Movie" } + val fileContent = movieType.toString() + + assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonDeserialize") + assertThat(fileContent).contains("tools.jackson.databind.`annotation`.JsonDeserialize") + + assertThat(fileContent).contains("tools.jackson.databind.`annotation`.JsonPOJOBuilder") + assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonPOJOBuilder") + + assertThat(fileContent).contains("@ToolsJacksonDatabindAnnotationJsonPOJOBuilder") + assertThat(fileContent).contains("@FasterxmlJacksonDatabindAnnotationJsonPOJOBuilder") + + assertThat(fileContent).contains("@ToolsJacksonDatabindAnnotationJsonDeserialize") + assertThat(fileContent).contains("@FasterxmlJacksonDatabindAnnotationJsonDeserialize") + } + + @Test + fun `defaults to Jackson 2 when no configuration is provided`() { + val result = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = "com.test", + language = Language.KOTLIN, + generateKotlinNullableClasses = true, + ), + ).generate() + + val movieType = result.kotlinDataTypes.first { it.name == "Movie" } + val fileContent = movieType.toString() + + assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonDeserialize") + assertThat(fileContent).doesNotContain("tools.jackson.databind.`annotation`.JsonDeserialize") + + assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonPOJOBuilder") + assertThat(fileContent).doesNotContain("tools.jackson.databind.`annotation`.JsonPOJOBuilder") + } + + @Test + fun `empty configuration defaults to Jackson 2`() { + val result = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = "com.test", + language = Language.KOTLIN, + generateKotlinNullableClasses = true, + jacksonVersions = emptySet(), + ), + ).generate() + + val movieType = result.kotlinDataTypes.first { it.name == "Movie" } + val fileContent = movieType.toString() + + // Should default to Jackson 2 (backwards compatibility) + assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonDeserialize") + assertThat(fileContent).doesNotContain("tools.jackson.databind.`annotation`.JsonDeserialize") + + assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonPOJOBuilder") + assertThat(fileContent).doesNotContain("tools.jackson.databind.`annotation`.JsonPOJOBuilder") + } +} diff --git a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt index cb44de469..fc4f55dfc 100644 --- a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt +++ b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt @@ -22,8 +22,10 @@ import com.netflix.graphql.dgs.codegen.CodeGen import com.netflix.graphql.dgs.codegen.CodeGenConfig import com.netflix.graphql.dgs.codegen.Language import org.gradle.api.DefaultTask +import org.gradle.api.artifacts.Configuration import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property import org.gradle.api.tasks.* import org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapper import java.io.File @@ -174,6 +176,12 @@ open class GenerateJavaTask project.configurations.findByName("dgsCodegen"), ) + @get:Internal + val compileClasspathConfiguration: Property = + objectFactory.property(Configuration::class.java).apply { + project.configurations.findByName("compileClasspath")?.let { convention(it) } + } + @TaskAction fun generate() { val schemaJarFilesFromDependencies = dgsCodegenClasspath.files.toList() @@ -186,6 +194,9 @@ open class GenerateJavaTask logger.info("Processing $it") } + val jacksonVersions = + compileClasspathConfiguration.orNull?.let { JacksonVersionDetector.detect(it) } ?: emptySet() + val config = CodeGenConfig( schemas = emptySet(), @@ -229,6 +240,7 @@ open class GenerateJavaTask javaGenerateAllConstructor = javaGenerateAllConstructor, trackInputFieldSet = trackInputFieldSet, generateJSpecifyAnnotations = generateJSpecifyAnnotations, + jacksonVersions = jacksonVersions, ) logger.info("Codegen config: {}", config) diff --git a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/JacksonVersionDetector.kt b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/JacksonVersionDetector.kt new file mode 100644 index 000000000..67308900d --- /dev/null +++ b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/JacksonVersionDetector.kt @@ -0,0 +1,49 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.graphql.dgs.codegen.gradle + +import com.netflix.graphql.dgs.codegen.JacksonVersion +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.component.ModuleComponentIdentifier + +object JacksonVersionDetector { + private const val JACKSON_2_GROUP = "com.fasterxml.jackson.core" + private const val JACKSON_3_GROUP = "tools.jackson.core" + private const val JACKSON_DATABIND_MODULE = "jackson-databind" + + /** + * Inspect a configuration's resolved components and report which Jackson major versions + * are on the classpath. Uses `incoming.resolutionResult` rather than + * `resolvedConfiguration.resolvedArtifacts` to avoid eager artifact download, which can + * break plugins (e.g. the Jakarta EE migration plugin) and multi-module projects. + */ + fun detect(configuration: Configuration): Set { + val versions = mutableSetOf() + configuration.incoming.resolutionResult.allComponents.forEach { comp -> + val id = comp.id + if (id !is ModuleComponentIdentifier) return@forEach + if (id.module != JACKSON_DATABIND_MODULE) return@forEach + when (id.group) { + JACKSON_2_GROUP -> versions += JacksonVersion.JACKSON_2 + JACKSON_3_GROUP -> versions += JacksonVersion.JACKSON_3 + } + } + return versions + } +} From 562610820cfba7acc5aa387e91bf254ff0b32494 Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Tue, 2 Jun 2026 14:48:03 -0700 Subject: [PATCH 2/4] Make Jackson detection compatible with config cache --- .../generators/kotlin/KotlinPoetUtils.kt | 5 +-- .../dgs/codegen/gradle/GenerateJavaTask.kt | 23 ++++++------- .../codegen/gradle/JacksonVersionDetector.kt | 29 +++++++--------- .../graphql/dgs/CodegenGradlePluginTest.kt | 33 +++++++++++++++++++ 4 files changed, 58 insertions(+), 32 deletions(-) diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinPoetUtils.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinPoetUtils.kt index ee231bf9b..f06f436c2 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinPoetUtils.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinPoetUtils.kt @@ -126,10 +126,7 @@ fun jsonSubTypesAnnotation(subTypes: Collection): AnnotationSpec { .build() } -/** - * Returns the configured Jackson versions, defaulting to Jackson 2 when none are configured - * (for backwards compatibility — see [JacksonVersion]). - */ +/** Configured Jackson versions based on compile dependencies, defaulting to Jackson 2 when none are set. */ private fun jacksonVersionsOrDefault(config: CodeGenConfig): Set = config.jacksonVersions.ifEmpty { setOf(JacksonVersion.JACKSON_2) } diff --git a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt index fc4f55dfc..ea17bf8ec 100644 --- a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt +++ b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt @@ -20,12 +20,13 @@ package com.netflix.graphql.dgs.codegen.gradle import com.netflix.graphql.dgs.codegen.CodeGen import com.netflix.graphql.dgs.codegen.CodeGenConfig +import com.netflix.graphql.dgs.codegen.JacksonVersion import com.netflix.graphql.dgs.codegen.Language import org.gradle.api.DefaultTask -import org.gradle.api.artifacts.Configuration import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.model.ObjectFactory -import org.gradle.api.provider.Property +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.* import org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapper import java.io.File @@ -176,11 +177,14 @@ open class GenerateJavaTask project.configurations.findByName("dgsCodegen"), ) - @get:Internal - val compileClasspathConfiguration: Property = - objectFactory.property(Configuration::class.java).apply { - project.configurations.findByName("compileClasspath")?.let { convention(it) } - } + @Input + val jacksonVersions: SetProperty = + objectFactory.setProperty(JacksonVersion::class.java).convention( + project.configurations + .named(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME) + .map { it.incoming.resolutionResult.allComponents } + .map { JacksonVersionDetector.detect(it) }, + ) @TaskAction fun generate() { @@ -194,9 +198,6 @@ open class GenerateJavaTask logger.info("Processing $it") } - val jacksonVersions = - compileClasspathConfiguration.orNull?.let { JacksonVersionDetector.detect(it) } ?: emptySet() - val config = CodeGenConfig( schemas = emptySet(), @@ -240,7 +241,7 @@ open class GenerateJavaTask javaGenerateAllConstructor = javaGenerateAllConstructor, trackInputFieldSet = trackInputFieldSet, generateJSpecifyAnnotations = generateJSpecifyAnnotations, - jacksonVersions = jacksonVersions, + jacksonVersions = jacksonVersions.get(), ) logger.info("Codegen config: {}", config) diff --git a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/JacksonVersionDetector.kt b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/JacksonVersionDetector.kt index 67308900d..129fac23f 100644 --- a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/JacksonVersionDetector.kt +++ b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/JacksonVersionDetector.kt @@ -19,8 +19,7 @@ package com.netflix.graphql.dgs.codegen.gradle import com.netflix.graphql.dgs.codegen.JacksonVersion -import org.gradle.api.artifacts.Configuration -import org.gradle.api.artifacts.component.ModuleComponentIdentifier +import org.gradle.api.artifacts.result.ResolvedComponentResult object JacksonVersionDetector { private const val JACKSON_2_GROUP = "com.fasterxml.jackson.core" @@ -28,22 +27,18 @@ object JacksonVersionDetector { private const val JACKSON_DATABIND_MODULE = "jackson-databind" /** - * Inspect a configuration's resolved components and report which Jackson major versions - * are on the classpath. Uses `incoming.resolutionResult` rather than - * `resolvedConfiguration.resolvedArtifacts` to avoid eager artifact download, which can - * break plugins (e.g. the Jakarta EE migration plugin) and multi-module projects. + * Which Jackson major versions are present among the resolved [components] + * (pass `resolutionResult.allComponents`). Reads graph metadata only — no artifact download. */ - fun detect(configuration: Configuration): Set { - val versions = mutableSetOf() - configuration.incoming.resolutionResult.allComponents.forEach { comp -> - val id = comp.id - if (id !is ModuleComponentIdentifier) return@forEach - if (id.module != JACKSON_DATABIND_MODULE) return@forEach - when (id.group) { - JACKSON_2_GROUP -> versions += JacksonVersion.JACKSON_2 - JACKSON_3_GROUP -> versions += JacksonVersion.JACKSON_3 + fun detect(components: Set): Set = + buildSet { + for (component in components) { + val moduleVersion = component.moduleVersion ?: continue + if (moduleVersion.name != JACKSON_DATABIND_MODULE) continue + when (moduleVersion.group) { + JACKSON_2_GROUP -> add(JacksonVersion.JACKSON_2) + JACKSON_3_GROUP -> add(JacksonVersion.JACKSON_3) + } } } - return versions - } } diff --git a/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginTest.kt b/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginTest.kt index 2c24c9c45..98f4796a9 100644 --- a/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginTest.kt +++ b/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginTest.kt @@ -174,6 +174,39 @@ class CodegenGradlePluginTest { assertThat(File(EXPECTED_DEFAULT_PATH + "NotSchema.java").exists()).isFalse() } + @Test + fun generateJavaIsConfigurationCacheCompatible() { + val projectDir = File("src/test/resources/test-project/") + + fun run() = + GradleRunner + .create() + .withProjectDir(projectDir) + .withPluginClasspath() + .withArguments( + "--stacktrace", + "--configuration-cache", + "--configuration-cache-problems=fail", + "-c", + "smoke_test_settings.gradle", + "clean", + "generateJava", + ).forwardOutput() + .build() + + // Clear Gradle configuration cache before test run. + File(projectDir, ".gradle/configuration-cache").deleteRecursively() + + // First run must store the configuration cache (a non-cacheable task field, e.g. a live Configuration, would fail here). + val first = run() + assertThat(first.task(":generateJava")).extracting { it?.outcome }.isEqualTo(SUCCESS) + assertThat(first.output).contains("Configuration cache entry stored.") + + // Second run must reload and reuse the stored entry. + val second = run() + assertThat(second.output).contains("Reusing configuration cache.") + } + companion object { const val EXPECTED_PATH = "src/test/resources/test-project/build/graphql/generated/sources/dgs-codegen/com/netflix/testproject/graphql/types/" From 33adc9d612786f16b7857495f0b8baa93879c17d Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Mon, 22 Jun 2026 12:37:16 -0700 Subject: [PATCH 3/4] Use ResolutionResult.getRootComponent and walk graph, only detect if generateKotlinNullableClasses --- graphql-dgs-codegen-core/build.gradle | 1 - .../netflix/graphql/dgs/codegen/CodeGen.kt | 13 ++++ .../dgs/codegen/gradle/CodegenPlugin.kt | 4 +- .../dgs/codegen/gradle/GenerateJavaTask.kt | 31 +++++++--- .../codegen/gradle/JacksonVersionDetector.kt | 62 ++++++++++++++++--- .../graphql/dgs/CodegenGradlePluginTest.kt | 51 ++++++++++++++- .../graphql/dgs/JacksonVersionDetectorTest.kt | 54 ++++++++++++++++ .../build_with_invalid_jackson_version.gradle | 37 +++++++++++ .../build_with_jackson_override.gradle | 37 +++++++++++ .../build_with_nullable_classes.gradle | 38 ++++++++++++ ...smoke_test_settings_invalid_jackson.gradle | 20 ++++++ ...moke_test_settings_jackson_override.gradle | 20 ++++++ .../smoke_test_settings_nullable.gradle | 20 ++++++ 13 files changed, 365 insertions(+), 23 deletions(-) create mode 100644 graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/JacksonVersionDetectorTest.kt create mode 100644 graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_invalid_jackson_version.gradle create mode 100644 graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_jackson_override.gradle create mode 100644 graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_nullable_classes.gradle create mode 100644 graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_invalid_jackson.gradle create mode 100644 graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_jackson_override.gradle create mode 100644 graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_nullable.gradle diff --git a/graphql-dgs-codegen-core/build.gradle b/graphql-dgs-codegen-core/build.gradle index f9d38a5e9..2792da501 100644 --- a/graphql-dgs-codegen-core/build.gradle +++ b/graphql-dgs-codegen-core/build.gradle @@ -43,7 +43,6 @@ dependencies { testImplementation 'org.jetbrains.kotlin:kotlin-compiler' integTestImplementation 'com.fasterxml.jackson.module:jackson-module-kotlin' - integTestImplementation 'tools.jackson.core:jackson-databind:latest.release' } application { diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt index 3d72494c9..4122c4a91 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt @@ -624,6 +624,19 @@ enum class Language { enum class JacksonVersion { JACKSON_2, JACKSON_3, + ; + + companion object { + fun fromString(value: String): JacksonVersion = + when (value.trim()) { + "2" -> JACKSON_2 + "3" -> JACKSON_3 + else -> throw IllegalArgumentException( + "Invalid Jackson version '$value'. Supported values are \"2\" (com.fasterxml.jackson) " + + "and \"3\" (tools.jackson).", + ) + } + } } data class CodeGenResult( diff --git a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/CodegenPlugin.kt b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/CodegenPlugin.kt index 241b5c188..983d218de 100644 --- a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/CodegenPlugin.kt +++ b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/CodegenPlugin.kt @@ -45,11 +45,11 @@ class CodegenPlugin : Plugin { val sourceSets = if (GradleVersion.current() >= - GradleVersion.version("7.1") + GradleVersion.version("7.4") ) { javaExtension.sourceSets } else { - throw RuntimeException("Gradle versions < 7.1 are no longer supported by DGS Codegen. Please upgrade your Gradle version.") + throw RuntimeException("Gradle versions < 7.4 are no longer supported by DGS Codegen. Please upgrade your Gradle version.") } val mainSourceSet = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME) val outputDir = generateJavaTaskProvider.map(GenerateJavaTask::getOutputDir) diff --git a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt index ea17bf8ec..50f7f779d 100644 --- a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt +++ b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt @@ -26,7 +26,8 @@ import org.gradle.api.DefaultTask import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.model.ObjectFactory import org.gradle.api.plugins.JavaPlugin -import org.gradle.api.provider.SetProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Provider import org.gradle.api.tasks.* import org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapper import java.io.File @@ -178,13 +179,25 @@ open class GenerateJavaTask ) @Input - val jacksonVersions: SetProperty = - objectFactory.setProperty(JacksonVersion::class.java).convention( - project.configurations - .named(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME) - .map { it.incoming.resolutionResult.allComponents } - .map { JacksonVersionDetector.detect(it) }, - ) + val jacksonVersionOverride: ListProperty = + objectFactory.listProperty(String::class.java).convention(emptyList()) + + private val detectedJacksonVersions: Provider> = + project.configurations + .named(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME) + .flatMap { it.incoming.resolutionResult.rootComponent } + .map { JacksonVersionDetector.detect(it) } + + /** + * Effective Jackson versions for this run. Jackson annotations are only emitted and the + * override is only resolved when [generateKotlinNullableClasses] is enabled. + */ + private fun resolveJacksonVersions(): Set = + if (generateKotlinNullableClasses) { + JacksonVersionDetector.resolve(jacksonVersionOverride.get(), detectedJacksonVersions.get()) + } else { + emptySet() + } @TaskAction fun generate() { @@ -241,7 +254,7 @@ open class GenerateJavaTask javaGenerateAllConstructor = javaGenerateAllConstructor, trackInputFieldSet = trackInputFieldSet, generateJSpecifyAnnotations = generateJSpecifyAnnotations, - jacksonVersions = jacksonVersions.get(), + jacksonVersions = resolveJacksonVersions(), ) logger.info("Codegen config: {}", config) diff --git a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/JacksonVersionDetector.kt b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/JacksonVersionDetector.kt index 129fac23f..a18b6f01f 100644 --- a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/JacksonVersionDetector.kt +++ b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/JacksonVersionDetector.kt @@ -19,7 +19,10 @@ package com.netflix.graphql.dgs.codegen.gradle import com.netflix.graphql.dgs.codegen.JacksonVersion +import org.gradle.api.InvalidUserDataException +import org.gradle.api.artifacts.component.ComponentIdentifier import org.gradle.api.artifacts.result.ResolvedComponentResult +import org.gradle.api.artifacts.result.ResolvedDependencyResult object JacksonVersionDetector { private const val JACKSON_2_GROUP = "com.fasterxml.jackson.core" @@ -27,18 +30,57 @@ object JacksonVersionDetector { private const val JACKSON_DATABIND_MODULE = "jackson-databind" /** - * Which Jackson major versions are present among the resolved [components] - * (pass `resolutionResult.allComponents`). Reads graph metadata only — no artifact download. + * Which Jackson major versions are present in the resolved dependency graph reachable from [root] + * (pass `resolutionResult.rootComponent.get()`). */ - fun detect(components: Set): Set = - buildSet { - for (component in components) { - val moduleVersion = component.moduleVersion ?: continue - if (moduleVersion.name != JACKSON_DATABIND_MODULE) continue - when (moduleVersion.group) { - JACKSON_2_GROUP -> add(JacksonVersion.JACKSON_2) - JACKSON_3_GROUP -> add(JacksonVersion.JACKSON_3) + fun detect(root: ResolvedComponentResult): Set { + val detected = mutableSetOf() + val visited = mutableSetOf() + val queue = ArrayDeque().apply { addLast(root) } + + while (queue.isNotEmpty()) { + val component = queue.removeFirst() + if (!visited.add(component.id)) continue + + component.moduleVersion?.let { module -> + if (module.name == JACKSON_DATABIND_MODULE) { + when (module.group) { + JACKSON_2_GROUP -> detected.add(JacksonVersion.JACKSON_2) + JACKSON_3_GROUP -> detected.add(JacksonVersion.JACKSON_3) + } } } + // Nothing more the graph can tell us once every known major has been seen. + if (detected.size == JacksonVersion.entries.size) break + + component.dependencies + .filterIsInstance() + .forEach { queue.addLast(it.selected) } + } + + return detected + } + + /** + * Resolves the Jackson versions to target: the parsed [override] when non-empty, otherwise the + * auto-[detected] set. An empty override means "no override" and falls back to detection. + * + * @throws InvalidUserDataException (failing the build) if [override] contains a value other than `"2"` or `"3"`. + */ + fun resolve( + override: List, + detected: Set, + ): Set = + if (override.isEmpty()) { + detected + } else { + override + .map { raw -> + try { + JacksonVersion.fromString(raw) + } catch (e: IllegalArgumentException) { + throw InvalidUserDataException("Invalid 'jacksonVersionOverride' value. ${e.message}", e) + } + }.toSet() } } diff --git a/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginTest.kt b/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginTest.kt index 98f4796a9..7dae8c127 100644 --- a/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginTest.kt +++ b/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginTest.kt @@ -174,10 +174,57 @@ class CodegenGradlePluginTest { assertThat(File(EXPECTED_DEFAULT_PATH + "NotSchema.java").exists()).isFalse() } + @Test + fun jacksonVersionOverrideIsApplied() { + // A valid override (["2", "3"]) should be accepted and supersede classpath detection. + val result = + GradleRunner + .create() + .withProjectDir(File("src/test/resources/test-project/")) + .withPluginClasspath() + .withArguments( + "--stacktrace", + "-c", + "smoke_test_settings_jackson_override.gradle", + "-b", + "build_with_jackson_override.gradle", + "clean", + "generateJava", + ).forwardOutput() + .build() + + assertThat(result.task(":generateJava")).extracting { it?.outcome }.isEqualTo(SUCCESS) + } + + @Test + fun invalidJacksonVersionOverrideFailsTheBuild() { + // An unsupported override value should fail the build with a clear message. + val result = + GradleRunner + .create() + .withProjectDir(File("src/test/resources/test-project/")) + .withPluginClasspath() + .withArguments( + "--stacktrace", + "-c", + "smoke_test_settings_invalid_jackson.gradle", + "-b", + "build_with_invalid_jackson_version.gradle", + "clean", + "generateJava", + ).forwardOutput() + .buildAndFail() + + assertThat(result.output).contains("Invalid 'jacksonVersionOverride' value") + assertThat(result.output).contains("Invalid Jackson version '4'") + } + @Test fun generateJavaIsConfigurationCacheCompatible() { val projectDir = File("src/test/resources/test-project/") + // generateKotlinNullableClasses is enabled so Jackson version detection (the lazy + // rootComponent classpath walk) actually runs under the configuration cache. fun run() = GradleRunner .create() @@ -188,7 +235,9 @@ class CodegenGradlePluginTest { "--configuration-cache", "--configuration-cache-problems=fail", "-c", - "smoke_test_settings.gradle", + "smoke_test_settings_nullable.gradle", + "-b", + "build_with_nullable_classes.gradle", "clean", "generateJava", ).forwardOutput() diff --git a/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/JacksonVersionDetectorTest.kt b/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/JacksonVersionDetectorTest.kt new file mode 100644 index 000000000..89e656f6e --- /dev/null +++ b/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/JacksonVersionDetectorTest.kt @@ -0,0 +1,54 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.graphql.dgs + +import com.netflix.graphql.dgs.codegen.JacksonVersion +import com.netflix.graphql.dgs.codegen.gradle.JacksonVersionDetector +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.gradle.api.InvalidUserDataException +import org.junit.jupiter.api.Test + +class JacksonVersionDetectorTest { + @Test + fun `resolve falls back to the detected versions when no override is given`() { + val detected = setOf(JacksonVersion.JACKSON_2) + assertThat(JacksonVersionDetector.resolve(emptyList(), detected)).isEqualTo(detected) + } + + @Test + fun `resolve uses the override and ignores detection when an override is given`() { + assertThat(JacksonVersionDetector.resolve(listOf("3"), setOf(JacksonVersion.JACKSON_2))) + .containsExactly(JacksonVersion.JACKSON_3) + } + + @Test + fun `resolve supports targeting both Jackson versions`() { + assertThat(JacksonVersionDetector.resolve(listOf("2", "3"), emptySet())) + .containsExactlyInAnyOrder(JacksonVersion.JACKSON_2, JacksonVersion.JACKSON_3) + } + + @Test + fun `resolve rejects an unsupported override value with a build-failing user error`() { + assertThatThrownBy { JacksonVersionDetector.resolve(listOf("4"), emptySet()) } + .isInstanceOf(InvalidUserDataException::class.java) + .hasMessageContaining("Invalid 'jacksonVersionOverride' value") + .hasMessageContaining("Invalid Jackson version '4'") + } +} diff --git a/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_invalid_jackson_version.gradle b/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_invalid_jackson_version.gradle new file mode 100644 index 000000000..8bd8186f7 --- /dev/null +++ b/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_invalid_jackson_version.gradle @@ -0,0 +1,37 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +plugins { + id 'java' + id 'com.netflix.dgs.codegen' +} + +configurations { + // injected by Gradle Runner through test configuration, see CodegenGradlePluginTest + CodeGenConfiguration.exclude group: "com.netflix.graphql.dgs.codegen", module: "graphql-dgs-codegen-core" +} + +generateJava { + schemaPaths = ["${projectDir}/src/main/resources/schema"] + packageName = 'com.netflix.testproject.graphql' + generatedSourcesDir = "${projectDir}/build/graphql" + jacksonVersionOverride = ["4"] + generateKotlinNullableClasses = true +} + +codegen.clientCoreConventionsEnabled = false \ No newline at end of file diff --git a/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_jackson_override.gradle b/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_jackson_override.gradle new file mode 100644 index 000000000..6377b2ff5 --- /dev/null +++ b/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_jackson_override.gradle @@ -0,0 +1,37 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +plugins { + id 'java' + id 'com.netflix.dgs.codegen' +} + +configurations { + // injected by Gradle Runner through test configuration, see CodegenGradlePluginTest + CodeGenConfiguration.exclude group: "com.netflix.graphql.dgs.codegen", module: "graphql-dgs-codegen-core" +} + +generateJava { + schemaPaths = ["${projectDir}/src/main/resources/schema"] + packageName = 'com.netflix.testproject.graphql' + generatedSourcesDir = "${projectDir}/build/graphql" + jacksonVersionOverride = ["2", "3"] + generateKotlinNullableClasses = true +} + +codegen.clientCoreConventionsEnabled = false diff --git a/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_nullable_classes.gradle b/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_nullable_classes.gradle new file mode 100644 index 000000000..284e52c07 --- /dev/null +++ b/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_nullable_classes.gradle @@ -0,0 +1,38 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +plugins { + id 'java' + id 'com.netflix.dgs.codegen' +} + +configurations { + // injected by Gradle Runner through test configuration, see CodegenGradlePluginTest + CodeGenConfiguration.exclude group: "com.netflix.graphql.dgs.codegen", module: "graphql-dgs-codegen-core" +} + +// No jacksonVersionOverride: exercises the classpath-detection path. generateKotlinNullableClasses +// triggers detection (it is the only generation path that consumes the detected Jackson versions). +generateJava { + schemaPaths = ["${projectDir}/src/main/resources/schema"] + packageName = 'com.netflix.testproject.graphql' + generatedSourcesDir = "${projectDir}/build/graphql" + generateKotlinNullableClasses = true +} + +codegen.clientCoreConventionsEnabled = false diff --git a/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_invalid_jackson.gradle b/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_invalid_jackson.gradle new file mode 100644 index 000000000..aeffa386c --- /dev/null +++ b/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_invalid_jackson.gradle @@ -0,0 +1,20 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +include 'smoke_test_settings.gradle' +rootProject.buildFileName = 'build_with_invalid_jackson_version.gradle' diff --git a/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_jackson_override.gradle b/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_jackson_override.gradle new file mode 100644 index 000000000..226651f08 --- /dev/null +++ b/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_jackson_override.gradle @@ -0,0 +1,20 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +include 'smoke_test_settings.gradle' +rootProject.buildFileName = 'build_with_jackson_override.gradle' diff --git a/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_nullable.gradle b/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_nullable.gradle new file mode 100644 index 000000000..eedc9f87d --- /dev/null +++ b/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_nullable.gradle @@ -0,0 +1,20 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +include 'smoke_test_settings.gradle' +rootProject.buildFileName = 'build_with_nullable_classes.gradle' From 1098f1ec415ce58aa0705ed0d02ed0330351790e Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Mon, 22 Jun 2026 13:20:29 -0700 Subject: [PATCH 4/4] Add integTest for jackson 3 detection --- graphql-dgs-codegen-core/build.gradle | 1 + .../graphql/dgs/codegen/Kotlin2CodeGenTest.kt | 32 +++++++++++++ .../graphql/dgs/CodegenGradlePluginTest.kt | 33 +++++++++++++ .../test-project/build_with_jackson3.gradle | 46 +++++++++++++++++++ .../smoke_test_settings_jackson3.gradle | 20 ++++++++ 5 files changed, 132 insertions(+) create mode 100644 graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_jackson3.gradle create mode 100644 graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_jackson3.gradle diff --git a/graphql-dgs-codegen-core/build.gradle b/graphql-dgs-codegen-core/build.gradle index 2792da501..5a3aa5386 100644 --- a/graphql-dgs-codegen-core/build.gradle +++ b/graphql-dgs-codegen-core/build.gradle @@ -43,6 +43,7 @@ dependencies { testImplementation 'org.jetbrains.kotlin:kotlin-compiler' integTestImplementation 'com.fasterxml.jackson.module:jackson-module-kotlin' + integTestImplementation 'tools.jackson.core:jackson-databind:3.2.0' // explicit version to keep tests stable } application { diff --git a/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/Kotlin2CodeGenTest.kt b/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/Kotlin2CodeGenTest.kt index cbdcca3a4..4af71b5a4 100644 --- a/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/Kotlin2CodeGenTest.kt +++ b/graphql-dgs-codegen-core/src/integTest/kotlin/com/netflix/graphql/dgs/codegen/Kotlin2CodeGenTest.kt @@ -266,6 +266,38 @@ class Kotlin2CodeGenTest { assertThat(nameField.get(instance)).isEqualTo("John") } + @Test + fun `generated Jackson 3 annotations compile against Jackson 3 on the classpath`() { + val schema = + """ + type Query { + movies: [Movie] + } + + type Movie { + title: String + director: String + } + """.trimIndent() + + val result = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = "com.netflix.test.jackson3", + language = Language.KOTLIN, + generateKotlinNullableClasses = true, + jacksonVersions = setOf(JacksonVersion.JACKSON_3), + ), + ).generate() + + val movie = result.kotlinDataTypes.first { it.name == "Movie" }.toString() + assertThat(movie).contains("tools.jackson.databind.`annotation`.JsonDeserialize") + assertThat(movie).contains("tools.jackson.databind.`annotation`.JsonPOJOBuilder") + + assertCompilesKotlin(result) + } + companion object { @Suppress("unused") @JvmStatic diff --git a/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginTest.kt b/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginTest.kt index 7dae8c127..77976e7ae 100644 --- a/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginTest.kt +++ b/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginTest.kt @@ -219,6 +219,39 @@ class CodegenGradlePluginTest { assertThat(result.output).contains("Invalid Jackson version '4'") } + @Test + fun detectsJackson3FromCompileClasspath() { + // End-to-end detection: Jackson 3 is the only Jackson on the compile classpath + // and the generated Kotlin must use the tools.jackson annotation packages. + val projectDir = File("src/test/resources/test-project/") + val result = + GradleRunner + .create() + .withProjectDir(projectDir) + .withPluginClasspath() + .withArguments( + "--stacktrace", + "-c", + "smoke_test_settings_jackson3.gradle", + "-b", + "build_with_jackson3.gradle", + "clean", + "generateJava", + ).forwardOutput() + .build() + + assertThat(result.task(":generateJava")).extracting { it?.outcome }.isEqualTo(SUCCESS) + + val generatedTypes = + File(projectDir, "build/graphql/generated/sources/dgs-codegen/com/netflix/testproject/graphql/types") + .walk() + .filter { it.extension == "kt" } + .joinToString("\n") { it.readText() } + + assertThat(generatedTypes).contains("tools.jackson.databind") + assertThat(generatedTypes).doesNotContain("com.fasterxml.jackson.databind.`annotation`.JsonDeserialize") + } + @Test fun generateJavaIsConfigurationCacheCompatible() { val projectDir = File("src/test/resources/test-project/") diff --git a/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_jackson3.gradle b/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_jackson3.gradle new file mode 100644 index 000000000..95fce39f6 --- /dev/null +++ b/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_jackson3.gradle @@ -0,0 +1,46 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +plugins { + id 'java' + id 'com.netflix.dgs.codegen' +} + +repositories { + mavenCentral() +} + +configurations { + // injected by Gradle Runner through test configuration, see CodegenGradlePluginTest + CodeGenConfiguration.exclude group: "com.netflix.graphql.dgs.codegen", module: "graphql-dgs-codegen-core" +} + +dependencies { + // Jackson 3 only on the compile classpath: detection must pick JACKSON_3 over the JACKSON_2 default. + implementation 'tools.jackson.core:jackson-databind:3.2.0' +} + +generateJava { + schemaPaths = ["${projectDir}/src/main/resources/schema"] + packageName = 'com.netflix.testproject.graphql' + generatedSourcesDir = "${projectDir}/build/graphql" + language = 'KOTLIN' + generateKotlinNullableClasses = true +} + +codegen.clientCoreConventionsEnabled = false diff --git a/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_jackson3.gradle b/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_jackson3.gradle new file mode 100644 index 000000000..5ccd5a2a3 --- /dev/null +++ b/graphql-dgs-codegen-gradle/src/test/resources/test-project/smoke_test_settings_jackson3.gradle @@ -0,0 +1,20 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +include 'smoke_test_settings.gradle' +rootProject.buildFileName = 'build_with_jackson3.gradle'