Skip to content

Commit 5dfd192

Browse files
authored
Merge pull request #918 from Netflix/feature/jackson-3-support
Add Jackson 3 support for `@JsonDeserialize` and `@JsonPOJOBuilder`
2 parents 9738943 + ef989c1 commit 5dfd192

File tree

7 files changed

+325
-22
lines changed

7 files changed

+325
-22
lines changed

graphql-dgs-codegen-core/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ dependencies {
4343
testImplementation 'org.jetbrains.kotlin:kotlin-compiler'
4444

4545
integTestImplementation 'com.fasterxml.jackson.module:jackson-module-kotlin'
46+
integTestImplementation 'tools.jackson.core:jackson-databind:latest.release'
4647
}
4748

4849
application {

graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ package com.netflix.graphql.dgs.codegen
2020

2121
import com.netflix.graphql.dgs.codegen.generators.java.*
2222
import com.netflix.graphql.dgs.codegen.generators.kotlin.*
23+
import com.netflix.graphql.dgs.codegen.generators.kotlin.configureJacksonVersion
2324
import com.netflix.graphql.dgs.codegen.generators.kotlin2.*
2425
import com.netflix.graphql.dgs.codegen.generators.shared.DocFileSpec
2526
import com.netflix.graphql.dgs.codegen.generators.shared.DocGenerator
@@ -75,6 +76,8 @@ class CodeGen(
7576
)
7677

7778
fun generate(): CodeGenResult {
79+
configureJacksonVersion(config.jacksonVersions)
80+
7881
loadTypeMappingsFromDependencies()
7982

8083
val codeGenResult =
@@ -582,6 +585,7 @@ class CodeGenConfig(
582585
var addDeprecatedAnnotation: Boolean = false,
583586
var trackInputFieldSet: Boolean = false,
584587
var generateJSpecifyAnnotations: Boolean = false,
588+
var jacksonVersions: Set<JacksonVersion> = emptySet(),
585589
) {
586590
val packageNameClient: String = "$packageName.$subPackageNameClient"
587591

@@ -618,6 +622,11 @@ enum class Language {
618622
KOTLIN,
619623
}
620624

625+
enum class JacksonVersion {
626+
JACKSON_2,
627+
JACKSON_3,
628+
}
629+
621630
data class CodeGenResult(
622631
val javaDataTypes: List<JavaFile> = listOf(),
623632
val javaInterfaces: List<JavaFile> = listOf(),

graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin/KotlinPoetUtils.kt

Lines changed: 79 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,9 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
2222
import com.fasterxml.jackson.annotation.JsonProperty
2323
import com.fasterxml.jackson.annotation.JsonSubTypes
2424
import com.fasterxml.jackson.annotation.JsonTypeInfo
25-
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
26-
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder
2725
import com.netflix.graphql.dgs.codegen.CodeGen
2826
import com.netflix.graphql.dgs.codegen.CodeGenConfig
27+
import com.netflix.graphql.dgs.codegen.JacksonVersion
2928
import com.netflix.graphql.dgs.codegen.generators.shared.CodeGeneratorUtils.capitalized
3029
import com.netflix.graphql.dgs.codegen.generators.shared.PackageParserUtil
3130
import com.netflix.graphql.dgs.codegen.generators.shared.ParserConstants
@@ -46,6 +45,60 @@ import graphql.language.StringValue
4645
import graphql.language.Value
4746
import java.lang.IllegalArgumentException
4847

48+
private var configuredVersions: Set<JacksonVersion> = emptySet()
49+
50+
/**
51+
* Configure the Jackson versions from project configuration.
52+
* Called by the Gradle plugin after inspecting project dependencies.
53+
*/
54+
fun configureJacksonVersion(jacksonVersions: Set<JacksonVersion>) {
55+
configuredVersions = jacksonVersions
56+
}
57+
58+
/**
59+
* Which Jackson versions to generate annotations for.
60+
*
61+
* 1. Use configured versions from Gradle plugin (inferred from project dependencies) if available
62+
* 2. Default to Jackson 2 for backwards compatibility
63+
*/
64+
private fun getJacksonVersions(): Set<JacksonVersion> =
65+
// Use configured versions if available and non-empty
66+
configuredVersions.takeIf { it.isNotEmpty() } ?: setOf(JacksonVersion.JACKSON_2)
67+
68+
/**
69+
* Get the ClassName(s) for JsonDeserialize annotation based on detected Jackson version(s).
70+
* Jackson 2: com.fasterxml.jackson.databind.annotation.JsonDeserialize
71+
* Jackson 3: tools.jackson.databind.annotation.JsonDeserialize
72+
*/
73+
private fun getJsonDeserializeClasses(): List<ClassName> {
74+
val versions = getJacksonVersions()
75+
return buildList {
76+
if (JacksonVersion.JACKSON_2 in versions) {
77+
add(ClassName("com.fasterxml.jackson.databind.annotation", "JsonDeserialize"))
78+
}
79+
if (JacksonVersion.JACKSON_3 in versions) {
80+
add(ClassName("tools.jackson.databind.annotation", "JsonDeserialize"))
81+
}
82+
}
83+
}
84+
85+
/**
86+
* Get the ClassName(s) for JsonPOJOBuilder annotation based on detected Jackson version(s).
87+
* Jackson 2: com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder
88+
* Jackson 3: tools.jackson.databind.annotation.JsonPOJOBuilder
89+
*/
90+
private fun getJsonPOJOBuilderClasses(): List<ClassName> {
91+
val versions = getJacksonVersions()
92+
return buildList {
93+
if (JacksonVersion.JACKSON_2 in versions) {
94+
add(ClassName("com.fasterxml.jackson.databind.annotation", "JsonPOJOBuilder"))
95+
}
96+
if (JacksonVersion.JACKSON_3 in versions) {
97+
add(ClassName("tools.jackson.databind.annotation", "JsonPOJOBuilder"))
98+
}
99+
}
100+
}
101+
49102
fun sanitizeKotlinIdentifier(name: String): String =
50103
if (name == "_") {
51104
"underscoreField_"
@@ -134,25 +187,38 @@ fun jsonSubTypesAnnotation(subTypes: Collection<ClassName>): AnnotationSpec {
134187
* ```
135188
* @JsonDeserialize(builder = Movie.Builder::class)
136189
* ```
190+
* ```
191+
* @FasterxmlJacksonDatabindAnnotationJsonDeserialize(builder = Movie.Builder::class)
192+
* @ToolsJacksonDatabindAnnotationJsonDeserialize(builder = Movie.Builder::class)
193+
* ```
137194
*/
138-
fun jsonDeserializeAnnotation(builderType: ClassName): AnnotationSpec =
139-
AnnotationSpec
140-
.builder(JsonDeserialize::class)
141-
.addMember("builder = %T::class", builderType)
142-
.build()
195+
fun jsonDeserializeAnnotations(builderType: ClassName): List<AnnotationSpec> =
196+
getJsonDeserializeClasses().map { jsonDeserializeClass ->
197+
AnnotationSpec
198+
.builder(jsonDeserializeClass)
199+
.addMember("builder = %T::class", builderType)
200+
.build()
201+
}
143202

144203
/**
145-
* Generate a [JsonPOJOBuilder] annotation for the builder class.
204+
* Generate [JsonPOJOBuilder] annotations for all available Jackson versions.
205+
* Jackson 2: com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder
206+
* Jackson 3: tools.jackson.databind.annotation.JsonPOJOBuilder
207+
* When both versions are detected, generates annotations for both.
146208
*
147-
* Example generated annotation:
209+
* Example generated annotations:
148210
* ```
149211
* @JsonPOJOBuilder
150212
* ```
213+
* ```
214+
* @FasterxmlJacksonDatabindAnnotationJsonPOJOBuilder
215+
* @ToolsJacksonDatabindAnnotationJsonPOJOBuilder
216+
* ```
151217
*/
152-
fun jsonBuilderAnnotation(): AnnotationSpec =
153-
AnnotationSpec
154-
.builder(JsonPOJOBuilder::class)
155-
.build()
218+
fun jsonBuilderAnnotations(): List<AnnotationSpec> =
219+
getJsonPOJOBuilderClasses().map { jsonPOJOBuilderClass ->
220+
AnnotationSpec.builder(jsonPOJOBuilderClass).build()
221+
}
156222

157223
/**
158224
* Generate a [JvmName] annotation for a kotlin property.

graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2DataTypes.kt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import com.netflix.graphql.dgs.codegen.generators.kotlin.ReservedKeywordFilter
2424
import com.netflix.graphql.dgs.codegen.generators.kotlin.addControlFlow
2525
import com.netflix.graphql.dgs.codegen.generators.kotlin.addOptionalGeneratedAnnotation
2626
import com.netflix.graphql.dgs.codegen.generators.kotlin.disableJsonTypeInfoAnnotation
27-
import com.netflix.graphql.dgs.codegen.generators.kotlin.jsonBuilderAnnotation
28-
import com.netflix.graphql.dgs.codegen.generators.kotlin.jsonDeserializeAnnotation
27+
import com.netflix.graphql.dgs.codegen.generators.kotlin.jsonBuilderAnnotations
28+
import com.netflix.graphql.dgs.codegen.generators.kotlin.jsonDeserializeAnnotations
2929
import com.netflix.graphql.dgs.codegen.generators.kotlin.jsonIgnorePropertiesAnnotation
3030
import com.netflix.graphql.dgs.codegen.generators.kotlin.jsonPropertyAnnotation
3131
import com.netflix.graphql.dgs.codegen.generators.kotlin.jvmNameAnnotation
@@ -134,8 +134,10 @@ fun generateKotlin2DataTypes(
134134
TypeSpec
135135
.classBuilder("Builder")
136136
.addOptionalGeneratedAnnotation(config)
137-
.addAnnotation(jsonBuilderAnnotation())
138-
.addAnnotation(jsonIgnorePropertiesAnnotation("__typename"))
137+
.apply {
138+
jsonBuilderAnnotations().forEach { addAnnotation(it) }
139+
addAnnotation(jsonIgnorePropertiesAnnotation("__typename"))
140+
}
139141
// add a backing property for each field
140142
.addProperties(
141143
fields.map { field ->
@@ -193,11 +195,9 @@ fun generateKotlin2DataTypes(
193195
}
194196
// add jackson annotations
195197
.addAnnotation(disableJsonTypeInfoAnnotation())
196-
.addAnnotation(
197-
jsonDeserializeAnnotation(
198-
builderClassName,
199-
),
200-
)
198+
.apply {
199+
jsonDeserializeAnnotations(builderClassName).forEach { addAnnotation(it) }
200+
}
201201
// add nested classes
202202
.addType(companionObject)
203203
.addType(builder)
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
*
3+
* Copyright 2020 Netflix, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package com.netflix.graphql.dgs.codegen
20+
21+
import org.assertj.core.api.Assertions.assertThat
22+
import org.junit.jupiter.api.Test
23+
24+
class JacksonVersionDetectionTest {
25+
private val schema =
26+
"""
27+
type Query {
28+
movies: [Movie]
29+
}
30+
31+
type Movie {
32+
title: String
33+
director: String
34+
}
35+
""".trimIndent()
36+
37+
@Test
38+
fun `generates only Jackson 2 JsonDeserialize and JsonPOJOBuilder annotations when Jackson 2 is configured`() {
39+
val result =
40+
CodeGen(
41+
CodeGenConfig(
42+
schemas = setOf(schema),
43+
packageName = "com.test",
44+
language = Language.KOTLIN,
45+
generateKotlinNullableClasses = true,
46+
jacksonVersions = setOf(JacksonVersion.JACKSON_2),
47+
),
48+
).generate()
49+
50+
val movieType = result.kotlinDataTypes.first { it.name == "Movie" }
51+
val fileContent = movieType.toString()
52+
53+
assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonDeserialize")
54+
assertThat(fileContent).doesNotContain("tools.jackson.databind.`annotation`.JsonDeserialize")
55+
56+
assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonPOJOBuilder")
57+
assertThat(fileContent).doesNotContain("tools.jackson.databind.`annotation`.JsonPOJOBuilder")
58+
}
59+
60+
@Test
61+
fun `generates only Jackson 3 JsonDeserialize and JsonPOJOBuilder annotations when Jackson 3 is configured`() {
62+
val result =
63+
CodeGen(
64+
CodeGenConfig(
65+
schemas = setOf(schema),
66+
packageName = "com.test",
67+
language = Language.KOTLIN,
68+
generateKotlinNullableClasses = true,
69+
jacksonVersions = setOf(JacksonVersion.JACKSON_3),
70+
),
71+
).generate()
72+
73+
val movieType = result.kotlinDataTypes.first { it.name == "Movie" }
74+
val fileContent = movieType.toString()
75+
76+
assertThat(fileContent).contains("tools.jackson.databind.`annotation`.JsonDeserialize")
77+
assertThat(fileContent).doesNotContain("com.fasterxml.jackson.databind.`annotation`.JsonDeserialize")
78+
79+
assertThat(fileContent).contains("tools.jackson.databind.`annotation`.JsonPOJOBuilder")
80+
assertThat(fileContent).doesNotContain("com.fasterxml.jackson.databind.`annotation`.JsonPOJOBuilder")
81+
}
82+
83+
@Test
84+
fun `generates both Jackson 2 and 3 JsonDeserialize and JsonPOJOBuilder annotations when both are configured`() {
85+
val result =
86+
CodeGen(
87+
CodeGenConfig(
88+
schemas = setOf(schema),
89+
packageName = "com.test",
90+
language = Language.KOTLIN,
91+
generateKotlinNullableClasses = true,
92+
jacksonVersions = setOf(JacksonVersion.JACKSON_2, JacksonVersion.JACKSON_3),
93+
),
94+
).generate()
95+
96+
val movieType = result.kotlinDataTypes.first { it.name == "Movie" }
97+
val fileContent = movieType.toString()
98+
99+
assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonDeserialize")
100+
assertThat(fileContent).contains("tools.jackson.databind.`annotation`.JsonDeserialize")
101+
102+
assertThat(fileContent).contains("tools.jackson.databind.`annotation`.JsonPOJOBuilder")
103+
assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonPOJOBuilder")
104+
105+
assertThat(fileContent).contains("@ToolsJacksonDatabindAnnotationJsonPOJOBuilder")
106+
assertThat(fileContent).contains("@FasterxmlJacksonDatabindAnnotationJsonPOJOBuilder")
107+
108+
assertThat(fileContent).contains("@ToolsJacksonDatabindAnnotationJsonDeserialize")
109+
assertThat(fileContent).contains("@FasterxmlJacksonDatabindAnnotationJsonDeserialize")
110+
}
111+
112+
@Test
113+
fun `defaults to Jackson 2 when no configuration is provided`() {
114+
val result =
115+
CodeGen(
116+
CodeGenConfig(
117+
schemas = setOf(schema),
118+
packageName = "com.test",
119+
language = Language.KOTLIN,
120+
generateKotlinNullableClasses = true,
121+
),
122+
).generate()
123+
124+
val movieType = result.kotlinDataTypes.first { it.name == "Movie" }
125+
val fileContent = movieType.toString()
126+
127+
assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonDeserialize")
128+
assertThat(fileContent).doesNotContain("tools.jackson.databind.`annotation`.JsonDeserialize")
129+
130+
assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonPOJOBuilder")
131+
assertThat(fileContent).doesNotContain("tools.jackson.databind.`annotation`.JsonPOJOBuilder")
132+
}
133+
134+
@Test
135+
fun `empty configuration defaults to Jackson 2`() {
136+
val result =
137+
CodeGen(
138+
CodeGenConfig(
139+
schemas = setOf(schema),
140+
packageName = "com.test",
141+
language = Language.KOTLIN,
142+
generateKotlinNullableClasses = true,
143+
jacksonVersions = emptySet(),
144+
),
145+
).generate()
146+
147+
val movieType = result.kotlinDataTypes.first { it.name == "Movie" }
148+
val fileContent = movieType.toString()
149+
150+
// Should default to Jackson 2 (backwards compatibility)
151+
assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonDeserialize")
152+
assertThat(fileContent).doesNotContain("tools.jackson.databind.`annotation`.JsonDeserialize")
153+
154+
assertThat(fileContent).contains("com.fasterxml.jackson.databind.`annotation`.JsonPOJOBuilder")
155+
assertThat(fileContent).doesNotContain("tools.jackson.databind.`annotation`.JsonPOJOBuilder")
156+
}
157+
}

graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ open class GenerateJavaTask
182182
logger.info("Processing $it")
183183
}
184184

185+
val jacksonVersions = JacksonVersionDetector.detectVersions(project)
186+
185187
val config =
186188
CodeGenConfig(
187189
schemas = emptySet(),
@@ -224,6 +226,7 @@ open class GenerateJavaTask
224226
javaGenerateAllConstructor = javaGenerateAllConstructor,
225227
trackInputFieldSet = trackInputFieldSet,
226228
generateJSpecifyAnnotations = generateJSpecifyAnnotations,
229+
jacksonVersions = jacksonVersions,
227230
)
228231

229232
logger.info("Codegen config: {}", config)

0 commit comments

Comments
 (0)