Skip to content

Add Jackson 3 support#945

Open
jiholee17 wants to merge 4 commits into
masterfrom
feature/add-jackson-3-support
Open

Add Jackson 3 support#945
jiholee17 wants to merge 4 commits into
masterfrom
feature/add-jackson-3-support

Conversation

@jiholee17

@jiholee17 jiholee17 commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Closes #899

Changes

This PR re-introduces support for Jackson 3 (now configured by default in Spring Boot 4 projects) for projects that use the Kotlin 2 generator (generateKotlinNullableClasses = true) since these are the ones that use the builder, by detecting which versions of Jackson annotations are available on the compile classpath and adding annotations for the available version(s). If neither version is found, codegen defaults to Jackson 2 annotations for backwards compatibility. If both versions are found, codegen adds both annotations and defers to runtime selection behavior.

Affected annotations are:

  • @JsonDeserialize in com.fasterxml.jackson.databind.annotation (Jackson 2) and tools.jackson.databind.annotation (Jackson 3)
  • @JsonPOJOBuilder in com.fasterxml.jackson.databind.annotation (Jackson 2) and tools.jackson.databind.annotation (Jackson 3)

Examples

Example with both Jackson versions:

import com.fasterxml.jackson.databind.`annotation`.JsonDeserialize as FasterxmlJacksonDatabindAnnotationJsonDeserialize
import tools.jackson.databind.`annotation`.JsonDeserialize as ToolsJacksonDatabindAnnotationJsonDeserialize
...
@JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
@FasterxmlJacksonDatabindAnnotationJsonDeserialize(builder = Country.Builder::class)
@ToolsJacksonDatabindAnnotationJsonDeserialize(builder = Country.Builder::class)
public class Country(
...

Example with single Jackson version:

import com.fasterxml.jackson.databind.`annotation`.JsonDeserialize
...
@JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
@JsonDeserialize(builder = Query.Builder::class)
public class Query(
...

How Jackson version detection works

The task has two inputs that decide which Jackson annotations get generated (@JsonDeserialize/@JsonPOJOBuilder):

  • jacksonVersionOverride — optional user override: ["2"] (com.fasterxml.jackson), ["3"] (tools.jackson), or ["2", "3"]. Anything else fails the build. Empty = auto-detect.
  • auto-detection — walks the resolved compileClasspath for the jackson-databind module and keys on group (com.fasterxml.jackson.core → 2, tools.jackson.core → 3; the group is the only discriminator since both share the artifact name).

The override wins when set; otherwise detection is used. This only runs when generateKotlinNullableClasses is enabled (the only path that emits Jackson annotations), and an empty result defaults to Jackson 2.

Detection uses a lazy rootComponent provider so it's configuration-cache safe, which raises the minimum Gradle version to 7.4.

Safe with the Jakarta EE migration plugin

Detection reads only the dependency graph metadata — it walks resolutionResult.rootComponent and never calls getFiles() / resolvedArtifacts. The Jakarta plugin is an artifact transform that only fires when artifact files are requested, so a metadata-only read never triggers it (and never forces sibling modules to build). That eager artifact-resolution path is what broke the earlier resolvedArtifacts-based attempt.

Safe with the Gradle configuration cache

The task never holds a Configuration or Project (neither is cc-serializable). Detection is a lazy rootComponent provider (replacing the earlier eager allComponents getter) that's resolved only at task execution — after all dependencies are declared — and the override is a plain ListProperty<String>, so everything serialized is cc-safe. A functional test runs generateJava twice with --configuration-cache --configuration-cache-problems=fail and asserts store-then-reuse

Comment thread graphql-dgs-codegen-core/build.gradle Outdated
)

@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

val jacksonVersions: SetProperty<JacksonVersion> =
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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Since the annotations are emitted into main source code, I think if we add annotations that aren't on the compile classpath but are on the runtime classpath for some reason, they wouldn't compile. I agree with adding a customer-facing property from your other comment to give the user a mechanism to override if the detection is incorrect and is more robust/correct than using runtime detection

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Jackson 3 support

2 participants