Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions graphql-dgs-codegen-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are there any integration tests that explicitly reference Jackson3? idk if having two sets (Jackson 2 and the same for Jackson 3 would be an overkill)

}

application {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,7 @@ class CodeGenConfig(
var addDeprecatedAnnotation: Boolean = false,
var trackInputFieldSet: Boolean = false,
var generateJSpecifyAnnotations: Boolean = false,
var jacksonVersions: Set<JacksonVersion> = emptySet(),
) {
val packageNameClient: String = "$packageName.$subPackageNameClient"

Expand Down Expand Up @@ -620,6 +621,11 @@ enum class Language {
KOTLIN,
}

enum class JacksonVersion {
JACKSON_2,
JACKSON_3,
}

data class CodeGenResult(
val javaDataTypes: List<JavaFile> = listOf(),
val javaInterfaces: List<JavaFile> = listOf(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -127,32 +126,55 @@ fun jsonSubTypesAnnotation(subTypes: Collection<ClassName>): AnnotationSpec {
.build()
}

/** Configured Jackson versions based on compile dependencies, defaulting to Jackson 2 when none are set. */
private fun jacksonVersionsOrDefault(config: CodeGenConfig): Set<JacksonVersion> =
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 a [JsonDeserialize] annotation for the builder class.
* 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<AnnotationSpec> =
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<AnnotationSpec> =
jacksonVersionsOrDefault(config).map { version ->
AnnotationSpec.builder(jsonPOJOBuilderClassName(version)).build()
}

/**
* Generate a [JvmName] annotation for a kotlin property.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +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.file.ConfigurableFileCollection
import org.gradle.api.model.ObjectFactory
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
Expand Down Expand Up @@ -174,6 +177,15 @@ open class GenerateJavaTask
project.configurations.findByName("dgsCodegen"),
)

@Input
val jacksonVersions: SetProperty<JacksonVersion> =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdyt of providing a customer-facing property that Gradle plugin consumers can override to specify the Jackson version to use if we detect it incorrectly (so they are not blocked)?

The con is that it's yet another prop that we need to maintain

objectFactory.setProperty(JacksonVersion::class.java).convention(
project.configurations
.named(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we consider runtime dependencies as well?

Thinking of a (weird) setup where Jackson is only present on the runtime classpath via transitive dependencies

.map { it.incoming.resolutionResult.allComponents }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line uses an eager getter that will trigger eager resolution at configuration time.
Gradle provides ResolutionResult.getRootComponent(): Provider<ResolvedComponentResult>:
https://docs.gradle.org/current/javadoc/org/gradle/api/artifacts/result/ResolutionResult.html
(since Gradle 7.4+ so it should be totally safe to use) as a config-cache-safe way to feed a resolution result into a task. Also better aligns with our code modernization efforts to play well with Gradle caching.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you switch to the ResolutionResult.getRootComponent() you'll also need to rewrite the detect() method to start walking from the root of the graph and keep track of visited nodes

.map { JacksonVersionDetector.detect(it) },
)

@TaskAction
fun generate() {
val schemaJarFilesFromDependencies = dgsCodegenClasspath.files.toList()
Expand Down Expand Up @@ -229,6 +241,7 @@ open class GenerateJavaTask
javaGenerateAllConstructor = javaGenerateAllConstructor,
trackInputFieldSet = trackInputFieldSet,
generateJSpecifyAnnotations = generateJSpecifyAnnotations,
jacksonVersions = jacksonVersions.get(),
)

logger.info("Codegen config: {}", config)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
*
* 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.result.ResolvedComponentResult

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"

/**
* Which Jackson major versions are present among the resolved [components]
* (pass `resolutionResult.allComponents`). Reads graph metadata only — no artifact download.
*/
fun detect(components: Set<ResolvedComponentResult>): Set<JacksonVersion> =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe add a couple of tests to cover various project setups (specifically thinking of muti-module project setups with various combinations of how and where plugin is declared vs. applied)

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)
}
}
}
}
Loading