From f06c780fea2ba15dffe6acb37bdfd35a95faa018 Mon Sep 17 00:00:00 2001
From: Pierre Villard
Date: Sun, 25 Jan 2026 04:34:20 +0100
Subject: [PATCH 1/2] Move to Jackson 3 - Phase 2
Design GitHubJackson interface with methods for reading/writing JSON
Implement GitHubJackson2 and GitHubJackson3
Create DefaultGitHubJackson factory with programmatic selection
Create GitHubJacksonException wrapper classes
Add GitHubBuilder.withJackson() for configuring Jackson implementation
Add testing infrastructure for both Jackson versions
---
pom.xml | 17 ++
src/main/java/org/kohsuke/github/GitHub.java | 9 +-
.../org/kohsuke/github/GitHubBuilder.java | 39 +++-
.../java/org/kohsuke/github/GitHubClient.java | 163 +++++++------
.../org/kohsuke/github/GitHubResponse.java | 4 -
.../github/internal/DefaultGitHubJackson.java | 97 ++++++++
.../github/internal/GitHubJackson.java | 134 +++++++++++
.../github/internal/GitHubJackson2.java | 194 ++++++++++++++++
.../github/internal/GitHubJackson3.java | 218 ++++++++++++++++++
.../internal/GitHubJacksonException.java | 50 ++++
.../org/kohsuke/github/GitHubJacksonTest.java | 63 +++++
.../internal/DefaultGitHubJacksonTest.java | 62 +++++
.../no-reflect-and-serialization-list | 5 +
13 files changed, 980 insertions(+), 75 deletions(-)
create mode 100644 src/main/java/org/kohsuke/github/internal/DefaultGitHubJackson.java
create mode 100644 src/main/java/org/kohsuke/github/internal/GitHubJackson.java
create mode 100644 src/main/java/org/kohsuke/github/internal/GitHubJackson2.java
create mode 100644 src/main/java/org/kohsuke/github/internal/GitHubJackson3.java
create mode 100644 src/main/java/org/kohsuke/github/internal/GitHubJacksonException.java
create mode 100644 src/test/java/org/kohsuke/github/GitHubJacksonTest.java
create mode 100644 src/test/java/org/kohsuke/github/internal/DefaultGitHubJacksonTest.java
diff --git a/pom.xml b/pom.xml
index e1fbffe519..9bf7939db3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -86,6 +86,7 @@
+
com.fasterxml.jackson
jackson-bom
@@ -114,6 +115,14 @@
pom
import
+
+
+ tools.jackson
+ jackson-bom
+ 3.0.4
+ pom
+ import
+
junit
junit
@@ -138,6 +147,7 @@
+
com.fasterxml.jackson.core
jackson-databind
@@ -196,6 +206,13 @@
commons-lang3
3.19.0
+
+
+
+ tools.jackson.core
+ jackson-databind
+ true
+
com.github.npathai
hamcrest-optional
diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java
index 9204660a92..69da63e1fe 100644
--- a/src/main/java/org/kohsuke/github/GitHub.java
+++ b/src/main/java/org/kohsuke/github/GitHub.java
@@ -30,6 +30,7 @@
import org.kohsuke.github.authorization.ImmutableAuthorizationProvider;
import org.kohsuke.github.authorization.UserAuthorizationProvider;
import org.kohsuke.github.connector.GitHubConnector;
+import org.kohsuke.github.internal.GitHubJackson;
import java.io.*;
import java.util.*;
@@ -379,6 +380,8 @@ private GitHub(GitHubClient client) {
* rateLimitChecker
* @param authorizationProvider
* a authorization provider
+ * @param jackson
+ * the Jackson implementation to use for JSON serialization
* @throws IOException
* Signals that an I/O exception has occurred.
*/
@@ -388,7 +391,8 @@ private GitHub(GitHubClient client) {
GitHubRateLimitHandler rateLimitHandler,
GitHubAbuseLimitHandler abuseLimitHandler,
GitHubRateLimitChecker rateLimitChecker,
- AuthorizationProvider authorizationProvider) throws IOException {
+ AuthorizationProvider authorizationProvider,
+ GitHubJackson jackson) throws IOException {
if (authorizationProvider instanceof DependentAuthorizationProvider) {
((DependentAuthorizationProvider) authorizationProvider).bind(this);
} else if (authorizationProvider instanceof ImmutableAuthorizationProvider
@@ -408,7 +412,8 @@ private GitHub(GitHubClient client) {
rateLimitHandler,
abuseLimitHandler,
rateLimitChecker,
- authorizationProvider);
+ authorizationProvider,
+ jackson);
// Ensure we have the login if it is available
// This preserves previously existing behavior. Consider removing in future.
diff --git a/src/main/java/org/kohsuke/github/GitHubBuilder.java b/src/main/java/org/kohsuke/github/GitHubBuilder.java
index 3f762fd059..02b2e796a3 100644
--- a/src/main/java/org/kohsuke/github/GitHubBuilder.java
+++ b/src/main/java/org/kohsuke/github/GitHubBuilder.java
@@ -5,6 +5,8 @@
import org.kohsuke.github.authorization.ImmutableAuthorizationProvider;
import org.kohsuke.github.connector.GitHubConnector;
import org.kohsuke.github.connector.GitHubConnectorResponse;
+import org.kohsuke.github.internal.DefaultGitHubJackson;
+import org.kohsuke.github.internal.GitHubJackson;
import java.io.File;
import java.io.FileInputStream;
@@ -159,6 +161,8 @@ static GitHubBuilder fromCredentials() throws IOException {
private GitHubConnector connector;
+ private GitHubJackson jackson;
+
private GitHubRateLimitChecker rateLimitChecker = new GitHubRateLimitChecker();
private GitHubRateLimitHandler rateLimitHandler = GitHubRateLimitHandler.WAIT;
@@ -189,7 +193,8 @@ public GitHub build() throws IOException {
rateLimitHandler,
abuseLimitHandler,
rateLimitChecker,
- authorizationProvider);
+ authorizationProvider,
+ jackson != null ? jackson : DefaultGitHubJackson.createDefault());
}
/**
@@ -277,6 +282,38 @@ public GitHubBuilder withEndpoint(String endpoint) {
return this;
}
+ /**
+ * Configures which Jackson implementation to use for JSON serialization/deserialization.
+ *
+ *
+ * By default, Jackson 2.x is used. To use Jackson 3.x, create a Jackson 3 instance using
+ * {@link DefaultGitHubJackson#createJackson3()} and pass it to this method.
+ *
+ *
+ * Example: Using Jackson 3.x
+ *
+ *
+ * GitHub github = new GitHubBuilder().withOAuthToken("token")
+ * .withJackson(DefaultGitHubJackson.createJackson3())
+ * .build();
+ *
+ *
+ *
+ * Note: To use Jackson 3.x, you must add the Jackson 3 {@code tools.jackson.core:jackson-databind}
+ * dependency to your project.
+ *
+ *
+ * @param jackson
+ * the Jackson implementation to use
+ * @return the GitHubBuilder
+ * @see DefaultGitHubJackson#createJackson2()
+ * @see DefaultGitHubJackson#createJackson3()
+ */
+ public GitHubBuilder withJackson(GitHubJackson jackson) {
+ this.jackson = jackson;
+ return this;
+ }
+
/**
* With jwt token GitHubBuilder.
*
diff --git a/src/main/java/org/kohsuke/github/GitHubClient.java b/src/main/java/org/kohsuke/github/GitHubClient.java
index 7963de57e2..573ebbba51 100644
--- a/src/main/java/org/kohsuke/github/GitHubClient.java
+++ b/src/main/java/org/kohsuke/github/GitHubClient.java
@@ -1,9 +1,8 @@
package org.kohsuke.github;
-import com.fasterxml.jackson.databind.*;
-import com.fasterxml.jackson.databind.introspect.VisibilityChecker;
-import com.fasterxml.jackson.databind.json.JsonMapper;
-import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.fasterxml.jackson.databind.InjectableValues;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.fasterxml.jackson.databind.ObjectWriter;
import org.apache.commons.io.IOUtils;
import org.kohsuke.github.authorization.AuthorizationProvider;
import org.kohsuke.github.authorization.UserAuthorizationProvider;
@@ -11,6 +10,8 @@
import org.kohsuke.github.connector.GitHubConnectorRequest;
import org.kohsuke.github.connector.GitHubConnectorResponse;
import org.kohsuke.github.function.FunctionThrows;
+import org.kohsuke.github.internal.GitHubJackson;
+import org.kohsuke.github.internal.GitHubJackson2;
import java.io.*;
import java.net.*;
@@ -25,8 +26,6 @@
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
-import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
-import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE;
import static java.net.HttpURLConnection.HTTP_ACCEPTED;
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
@@ -109,14 +108,17 @@ static class RetryRequestException extends IOException {
private static final int DEFAULT_MAXIMUM_RETRY_MILLIS = 100;
/** The Constant DEFAULT_MINIMUM_RETRY_TIMEOUT_MILLIS. */
private static final int DEFAULT_MINIMUM_RETRY_MILLIS = DEFAULT_MAXIMUM_RETRY_MILLIS;
+
+ /**
+ * Jackson 2.x specific implementation for backward compatibility with static methods.
+ *
+ * This is used by {@link #getMappingObjectReader(GitHubConnectorResponse)} and {@link #getMappingObjectWriter()} to
+ * maintain backward compatibility with code that expects Jackson 2.x ObjectReader/ObjectWriter.
+ *
+ */
+ private static final GitHubJackson2 JACKSON2_STATIC = new GitHubJackson2();
+
private static final Logger LOGGER = Logger.getLogger(GitHubClient.class.getName());
- private static final ObjectMapper MAPPER = JsonMapper.builder()
- .addModule(new JavaTimeModule())
- .visibility(new VisibilityChecker.Std(NONE, NONE, NONE, NONE, ANY))
- .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
- .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
- .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
- .build();
private static final ThreadLocal sendRequestTraceId = new ThreadLocal<>();
@@ -251,41 +253,6 @@ private static void logRetryConnectionError(IOException e, URL url, int retries)
}
}
- private static GitHubConnectorRequest prepareConnectorRequest(GitHubRequest request,
- AuthorizationProvider authorizationProvider) throws IOException {
- GitHubRequest.Builder> builder = request.toBuilder();
- // if the authentication is needed but no credential is given, try it anyway (so that some calls
- // that do work with anonymous access in the reduced form should still work.)
- if (!request.allHeaders().containsKey("Authorization")) {
- String authorization = authorizationProvider.getEncodedAuthorization();
- if (authorization != null) {
- builder.setHeader("Authorization", authorization);
- }
- }
- if (request.header("Accept") == null) {
- builder.setHeader("Accept", "application/vnd.github+json");
- }
- builder.setHeader("Accept-Encoding", "gzip");
-
- builder.setHeader("X-GitHub-Api-Version", "2022-11-28");
-
- if (request.hasBody()) {
- if (request.body() != null) {
- builder.contentType(defaultString(request.contentType(), "application/x-www-form-urlencoded"));
- } else {
- builder.contentType("application/json");
- Map json = new HashMap<>();
- for (GitHubRequest.Entry e : request.args()) {
- json.put(e.key, e.value);
- }
- builder.with(new ByteArrayInputStream(getMappingObjectWriter().writeValueAsBytes(json)));
- }
-
- }
-
- return builder.build();
- }
-
private static boolean shouldIgnoreBody(@Nonnull GitHubConnectorResponse connectorResponse) {
if (connectorResponse.statusCode() == HTTP_NOT_MODIFIED) {
// special case handling for 304 unmodified, as the content will be ""
@@ -311,25 +278,39 @@ private static boolean shouldIgnoreBody(@Nonnull GitHubConnectorResponse connect
/**
* Helper for {@link #getMappingObjectReader(GitHubConnectorResponse)}.
*
+ *
+ * Note: This method returns a Jackson 2.x {@link ObjectReader}. For Jackson 3.x compatibility, use
+ * the {@link GitHubJackson} abstraction instead.
+ *
+ *
* @param root
* the root GitHub object for this reader
* @return an {@link ObjectReader} instance that can be further configured.
*/
@Nonnull
static ObjectReader getMappingObjectReader(@Nonnull GitHub root) {
- ObjectReader reader = getMappingObjectReader((GitHubConnectorResponse) null);
- ((InjectableValues.Std) reader.getInjectableValues()).addValue(GitHub.class, root);
- return reader;
+ Map injected = JACKSON2_STATIC.createInjectableValues(null);
+ JACKSON2_STATIC.addGitHubRoot(injected, root);
+ return JACKSON2_STATIC.getReader(injected);
}
/**
* Gets an {@link ObjectReader}.
*
+ *
* Members of {@link InjectableValues} must be present even if {@code null}, otherwise classes expecting those
* values will fail to read. This differs from regular JSONProperties which provide defaults instead of failing.
+ *
*
+ *
* Having one spot to create readers and having it take all injectable values is not a great long term solution but
* it is sufficient for this first cut.
+ *
+ *
+ *
+ * Note: This method returns a Jackson 2.x {@link ObjectReader}. For Jackson 3.x compatibility, use
+ * the {@link GitHubJackson} abstraction instead.
+ *
*
* @param connectorResponse
* the {@link GitHubConnectorResponse} to inject for this reader.
@@ -338,31 +319,23 @@ static ObjectReader getMappingObjectReader(@Nonnull GitHub root) {
*/
@Nonnull
static ObjectReader getMappingObjectReader(@CheckForNull GitHubConnectorResponse connectorResponse) {
- Map injected = new HashMap<>();
-
- // Required or many things break
- injected.put(GitHubConnectorResponse.class.getName(), null);
- injected.put(GitHub.class.getName(), null);
-
- if (connectorResponse != null) {
- injected.put(GitHubConnectorResponse.class.getName(), connectorResponse);
- GitHubConnectorRequest request = connectorResponse.request();
- // This is cheating, but it is an acceptable cheat for now.
- if (request instanceof GitHubRequest) {
- injected.putAll(((GitHubRequest) connectorResponse.request()).injectedMappingValues());
- }
- }
- return MAPPER.reader(new InjectableValues.Std(injected));
+ Map injected = JACKSON2_STATIC.createInjectableValues(connectorResponse);
+ return JACKSON2_STATIC.getReader(injected);
}
/**
* Gets an {@link ObjectWriter}.
*
+ *
+ * Note: This method returns a Jackson 2.x {@link ObjectWriter}. For Jackson 3.x compatibility, use
+ * the {@link GitHubJackson} abstraction instead.
+ *
+ *
* @return an {@link ObjectWriter} instance that can be further configured.
*/
@Nonnull
static ObjectWriter getMappingObjectWriter() {
- return MAPPER.writer();
+ return JACKSON2_STATIC.getWriter();
}
/**
@@ -461,6 +434,11 @@ static Map unmodifiableMapOrNull(Map extends K, ? extends V> map)
private GitHubConnector connector;
+ /**
+ * The Jackson implementation used for JSON serialization/deserialization.
+ */
+ private final GitHubJackson jackson;
+
@Nonnull
private final AtomicReference rateLimit = new AtomicReference<>(GHRateLimit.DEFAULT);
@@ -489,13 +467,16 @@ static Map unmodifiableMapOrNull(Map extends K, ? extends V> map)
* the rate limit checker
* @param authorizationProvider
* the authorization provider
+ * @param jackson
+ * the Jackson implementation to use for JSON serialization
*/
GitHubClient(String apiUrl,
GitHubConnector connector,
GitHubRateLimitHandler rateLimitHandler,
GitHubAbuseLimitHandler abuseLimitHandler,
GitHubRateLimitChecker rateLimitChecker,
- AuthorizationProvider authorizationProvider) {
+ AuthorizationProvider authorizationProvider,
+ GitHubJackson jackson) {
if (apiUrl.endsWith("/")) {
apiUrl = apiUrl.substring(0, apiUrl.length() - 1); // normalize
@@ -513,6 +494,7 @@ static Map unmodifiableMapOrNull(Map extends K, ? extends V> map)
this.rateLimitHandler = rateLimitHandler;
this.abuseLimitHandler = abuseLimitHandler;
this.rateLimitChecker = rateLimitChecker;
+ this.jackson = jackson;
}
/**
@@ -860,6 +842,41 @@ private void noteRateLimit(@Nonnull RateLimitTarget rateLimitTarget,
}
}
+ private GitHubConnectorRequest prepareConnectorRequest(GitHubRequest request,
+ AuthorizationProvider authorizationProvider) throws IOException {
+ GitHubRequest.Builder> builder = request.toBuilder();
+ // if the authentication is needed but no credential is given, try it anyway (so that some calls
+ // that do work with anonymous access in the reduced form should still work.)
+ if (!request.allHeaders().containsKey("Authorization")) {
+ String authorization = authorizationProvider.getEncodedAuthorization();
+ if (authorization != null) {
+ builder.setHeader("Authorization", authorization);
+ }
+ }
+ if (request.header("Accept") == null) {
+ builder.setHeader("Accept", "application/vnd.github+json");
+ }
+ builder.setHeader("Accept-Encoding", "gzip");
+
+ builder.setHeader("X-GitHub-Api-Version", "2022-11-28");
+
+ if (request.hasBody()) {
+ if (request.body() != null) {
+ builder.contentType(defaultString(request.contentType(), "application/x-www-form-urlencoded"));
+ } else {
+ builder.contentType("application/json");
+ Map json = new HashMap<>();
+ for (GitHubRequest.Entry e : request.args()) {
+ json.put(e.key, e.value);
+ }
+ builder.with(new ByteArrayInputStream(jackson.writeValueAsBytes(json)));
+ }
+
+ }
+
+ return builder.build();
+ }
+
private GitHubConnectorRequest prepareRedirectRequest(GitHubConnectorResponse connectorResponse,
GitHubRequest request) throws IOException {
URI requestUri = URI.create(request.url().toString());
@@ -921,6 +938,16 @@ String getEncodedAuthorization() throws IOException {
return authorizationProvider.getEncodedAuthorization();
}
+ /**
+ * Gets the name of the Jackson implementation being used by this client.
+ *
+ * @return the implementation name, e.g., "Jackson 2.21.0" or "Jackson 3.0.3"
+ */
+ @Nonnull
+ String getJacksonImplementationName() {
+ return jackson.getImplementationName();
+ }
+
/**
* Gets the login.
*
diff --git a/src/main/java/org/kohsuke/github/GitHubResponse.java b/src/main/java/org/kohsuke/github/GitHubResponse.java
index 8ac65391f7..053ef548e2 100644
--- a/src/main/java/org/kohsuke/github/GitHubResponse.java
+++ b/src/main/java/org/kohsuke/github/GitHubResponse.java
@@ -1,7 +1,6 @@
package org.kohsuke.github;
import com.fasterxml.jackson.core.JsonParseException;
-import com.fasterxml.jackson.databind.InjectableValues;
import com.fasterxml.jackson.databind.JsonMappingException;
import org.apache.commons.io.IOUtils;
import org.kohsuke.github.connector.GitHubConnectorResponse;
@@ -95,9 +94,6 @@ static T parseBody(GitHubConnectorResponse connectorResponse, Class type)
String data = getBodyAsString(connectorResponse);
try {
- InjectableValues.Std inject = new InjectableValues.Std();
- inject.addValue(GitHubConnectorResponse.class, connectorResponse);
-
return GitHubClient.getMappingObjectReader(connectorResponse).forType(type).readValue(data);
} catch (JsonMappingException | JsonParseException e) {
String message = "Failed to deserialize: " + data;
diff --git a/src/main/java/org/kohsuke/github/internal/DefaultGitHubJackson.java b/src/main/java/org/kohsuke/github/internal/DefaultGitHubJackson.java
new file mode 100644
index 0000000000..1742116cf1
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/internal/DefaultGitHubJackson.java
@@ -0,0 +1,97 @@
+package org.kohsuke.github.internal;
+
+/**
+ * Factory class for creating {@link GitHubJackson} implementations.
+ *
+ *
+ * This factory provides methods to create Jackson 2.x or Jackson 3.x implementations for JSON
+ * serialization/deserialization.
+ *
+ *
+ * Usage
+ *
+ *
+ * By default, Jackson 2.x is used. To use Jackson 3.x, configure the {@link org.kohsuke.github.GitHubBuilder}:
+ *
+ *
+ *
+ * // Using Jackson 2.x (default)
+ * GitHub github = new GitHubBuilder().withOAuthToken("token").build();
+ *
+ * // Using Jackson 3.x
+ * GitHub github = new GitHubBuilder().withOAuthToken("token")
+ * .withJackson(DefaultGitHubJackson.createJackson3())
+ * .build();
+ *
+ *
+ * Jackson 3.x Dependencies
+ *
+ *
+ * To use Jackson 3.x, add the {@code tools.jackson.core:jackson-databind} dependency to your project.
+ *
+ *
+ * @author Pierre Villard
+ * @see GitHubJackson
+ * @see GitHubJackson2
+ * @see GitHubJackson3
+ */
+public final class DefaultGitHubJackson {
+
+ /**
+ * Creates the default {@link GitHubJackson} instance.
+ *
+ *
+ * This method returns a Jackson 2.x implementation, which is the default and most stable option.
+ *
+ *
+ * @return a GitHubJackson2 instance
+ */
+ public static GitHubJackson createDefault() {
+ return new GitHubJackson2();
+ }
+
+ /**
+ * Creates a Jackson 2.x implementation.
+ *
+ *
+ * Jackson 2.x uses the {@code com.fasterxml.jackson} package and is the default implementation.
+ *
+ *
+ * @return a GitHubJackson2 instance
+ */
+ public static GitHubJackson2 createJackson2() {
+ return new GitHubJackson2();
+ }
+
+ /**
+ * Creates a Jackson 3.x implementation.
+ *
+ *
+ * Jackson 3.x uses the {@code tools.jackson} package and requires the Jackson 3.x dependencies to be present on the
+ * classpath.
+ *
+ *
+ * @return a GitHubJackson3 instance
+ * @throws IllegalStateException
+ * if Jackson 3.x is not available on the classpath
+ */
+ public static GitHubJackson3 createJackson3() {
+ if (!isJackson3Available()) {
+ throw new IllegalStateException("Jackson 3.x is not available on the classpath. "
+ + "Please add tools.jackson.core:jackson-databind and tools.jackson.datatype:jackson-datatype-jsr310 dependencies.");
+ }
+ return new GitHubJackson3();
+ }
+
+ /**
+ * Checks if Jackson 3.x is available on the classpath.
+ *
+ * @return true if Jackson 3.x classes can be loaded
+ */
+ public static boolean isJackson3Available() {
+ return GitHubJackson3.isAvailable();
+ }
+
+ private DefaultGitHubJackson() {
+ }
+}
diff --git a/src/main/java/org/kohsuke/github/internal/GitHubJackson.java b/src/main/java/org/kohsuke/github/internal/GitHubJackson.java
new file mode 100644
index 0000000000..eab9290beb
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/internal/GitHubJackson.java
@@ -0,0 +1,134 @@
+package org.kohsuke.github.internal;
+
+import org.kohsuke.github.connector.GitHubConnectorResponse;
+
+import java.io.IOException;
+import java.util.Map;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+
+/**
+ * Interface for JSON serialization/deserialization operations.
+ *
+ *
+ * This interface abstracts Jackson-specific operations to allow supporting multiple Jackson versions (2.x and 3.x)
+ * simultaneously. Implementations handle the version-specific details while providing a consistent API.
+ *
+ *
+ * Available Implementations
+ *
+ * - {@link GitHubJackson2} - Jackson 2.x implementation (default)
+ * - {@link GitHubJackson3} - Jackson 3.x implementation (requires additional dependencies)
+ *
+ *
+ * Configuration
+ *
+ * Use {@link org.kohsuke.github.GitHubBuilder#withJackson(GitHubJackson)} to configure which Jackson version to use:
+ *
+ *
+ *
+ * // Use Jackson 3.x
+ * GitHub github = new GitHubBuilder().withJackson(DefaultGitHubJackson.createJackson3()).build();
+ *
+ *
+ * @author Pierre Villard
+ * @see DefaultGitHubJackson
+ * @see GitHubJackson2
+ * @see GitHubJackson3
+ */
+public interface GitHubJackson {
+
+ /**
+ * Creates injectable values map with standard GitHub API values pre-populated.
+ *
+ * @param connectorResponse
+ * the connector response (may be null)
+ * @return a map suitable for passing to read methods
+ */
+ @Nonnull
+ Map createInjectableValues(@CheckForNull GitHubConnectorResponse connectorResponse);
+
+ /**
+ * Gets the name/version of this Jackson implementation for logging purposes.
+ *
+ * @return a string identifying this implementation (e.g., "Jackson 2.21.0" or "Jackson 3.0.3")
+ */
+ @Nonnull
+ String getImplementationName();
+
+ /**
+ * Reads a JSON string into an object of the specified type.
+ *
+ * @param
+ * the type to deserialize to
+ * @param json
+ * the JSON string to parse
+ * @param type
+ * the target class
+ * @param injectedValues
+ * values to inject during deserialization (may be null)
+ * @return the deserialized object
+ * @throws IOException
+ * if there is an I/O error or parsing error
+ */
+ @CheckForNull
+ T readValue(@Nonnull String json, @Nonnull Class type, @CheckForNull Map injectedValues)
+ throws IOException;
+
+ /**
+ * Reads a JsonNode into an object of the specified type.
+ *
+ *
+ * This method handles the version-specific way of reading from a tree node. In Jackson 2.x, this uses
+ * {@code traverse()}, while in Jackson 3.x it uses a different approach.
+ *
+ *
+ * @param
+ * the type to deserialize to
+ * @param node
+ * the JSON node (JsonNode from either Jackson version)
+ * @param type
+ * the target class
+ * @param injectedValues
+ * values to inject during deserialization (may be null)
+ * @return the deserialized object
+ * @throws IOException
+ * if there is an I/O error or parsing error
+ */
+ @CheckForNull
+ T readValueFromNode(@Nonnull Object node,
+ @Nonnull Class type,
+ @CheckForNull Map injectedValues) throws IOException;
+
+ /**
+ * Reads a JSON string and updates an existing object instance.
+ *
+ * @param
+ * the type of the object
+ * @param json
+ * the JSON string to parse
+ * @param instance
+ * the object to update with parsed data
+ * @param injectedValues
+ * values to inject during deserialization (may be null)
+ * @return the updated object instance
+ * @throws IOException
+ * if there is an I/O error or parsing error
+ */
+ @CheckForNull
+ T readValueToUpdate(@Nonnull String json, @Nonnull T instance, @CheckForNull Map injectedValues)
+ throws IOException;
+
+ /**
+ * Writes an object to a JSON byte array.
+ *
+ * @param value
+ * the object to serialize
+ * @return the JSON as a byte array
+ * @throws IOException
+ * if there is an I/O error or serialization error
+ */
+ @Nonnull
+ byte[] writeValueAsBytes(@Nonnull Object value) throws IOException;
+}
diff --git a/src/main/java/org/kohsuke/github/internal/GitHubJackson2.java b/src/main/java/org/kohsuke/github/internal/GitHubJackson2.java
new file mode 100644
index 0000000000..a9d74cfb13
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/internal/GitHubJackson2.java
@@ -0,0 +1,194 @@
+package org.kohsuke.github.internal;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.TreeNode;
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.InjectableValues;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.introspect.VisibilityChecker;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.kohsuke.github.GitHub;
+import org.kohsuke.github.GitHubRequest;
+import org.kohsuke.github.connector.GitHubConnectorRequest;
+import org.kohsuke.github.connector.GitHubConnectorResponse;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+
+/**
+ * Jackson 2.x implementation of {@link GitHubJackson}.
+ *
+ *
+ * This implementation uses Jackson 2.x APIs ({@code com.fasterxml.jackson.*}).
+ *
+ *
+ * @author Pierre Villard
+ */
+public class GitHubJackson2 implements GitHubJackson {
+
+ private static final ObjectMapper MAPPER = JsonMapper.builder()
+ .addModule(new JavaTimeModule())
+ .visibility(new VisibilityChecker.Std(Visibility.NONE,
+ Visibility.NONE,
+ Visibility.NONE,
+ Visibility.NONE,
+ Visibility.ANY))
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
+ .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
+ .build();
+
+ private final String version;
+
+ /**
+ * Creates a new GitHubJackson2 instance.
+ */
+ public GitHubJackson2() {
+ Version jacksonVersion = MAPPER.version();
+ this.version = "Jackson " + jacksonVersion.getMajorVersion() + "." + jacksonVersion.getMinorVersion() + "."
+ + jacksonVersion.getPatchLevel();
+ }
+
+ /**
+ * Adds a value to the injectable values map for the GitHub root object.
+ *
+ * @param injectedValues
+ * the map to modify
+ * @param root
+ * the GitHub root object to add
+ */
+ public void addGitHubRoot(@Nonnull Map injectedValues, @Nonnull GitHub root) {
+ injectedValues.put(GitHub.class.getName(), root);
+ }
+
+ @Override
+ @Nonnull
+ public Map createInjectableValues(@CheckForNull GitHubConnectorResponse connectorResponse) {
+ Map injected = new HashMap<>();
+
+ // Required or many things break
+ injected.put(GitHubConnectorResponse.class.getName(), null);
+ injected.put(GitHub.class.getName(), null);
+
+ if (connectorResponse != null) {
+ injected.put(GitHubConnectorResponse.class.getName(), connectorResponse);
+ GitHubConnectorRequest request = connectorResponse.request();
+ // This is cheating, but it is an acceptable cheat for now.
+ // GitHubRequest has additional injectable values
+ if (request instanceof GitHubRequest) {
+ injected.putAll(((GitHubRequest) request).injectedMappingValues());
+ }
+ }
+ return injected;
+ }
+
+ @Override
+ @Nonnull
+ public String getImplementationName() {
+ return version;
+ }
+
+ /**
+ * Gets an ObjectReader configured with injectable values.
+ *
+ *
+ * This method is exposed for compatibility with code that still needs direct access to ObjectReader.
+ *
+ *
+ * @param injectedValues
+ * values to inject during deserialization
+ * @return a configured ObjectReader
+ */
+ @Nonnull
+ public ObjectReader getReader(@CheckForNull Map injectedValues) {
+ return createReader(injectedValues);
+ }
+
+ /**
+ * Gets an ObjectWriter.
+ *
+ *
+ * This method is exposed for compatibility with code that still needs direct access to ObjectWriter.
+ *
+ *
+ * @return an ObjectWriter
+ */
+ @Nonnull
+ public ObjectWriter getWriter() {
+ return MAPPER.writer();
+ }
+
+ @Override
+ @CheckForNull
+ public T readValue(@Nonnull String json,
+ @Nonnull Class type,
+ @CheckForNull Map injectedValues) throws IOException {
+ try {
+ ObjectReader reader = createReader(injectedValues);
+ return reader.forType(type).readValue(json);
+ } catch (JsonProcessingException e) {
+ throw new GitHubJacksonException("Failed to deserialize JSON", e);
+ }
+ }
+
+ @Override
+ @CheckForNull
+ public T readValueFromNode(@Nonnull Object node,
+ @Nonnull Class type,
+ @CheckForNull Map injectedValues) throws IOException {
+ if (!(node instanceof TreeNode)) {
+ throw new IllegalArgumentException("Node must be a Jackson 2.x TreeNode");
+ }
+ try {
+ ObjectReader reader = createReader(injectedValues);
+ return reader.forType(type).readValue(((TreeNode) node).traverse());
+ } catch (JsonProcessingException e) {
+ throw new GitHubJacksonException("Failed to deserialize JSON from node", e);
+ }
+ }
+
+ @Override
+ @CheckForNull
+ public T readValueToUpdate(@Nonnull String json,
+ @Nonnull T instance,
+ @CheckForNull Map injectedValues) throws IOException {
+ try {
+ ObjectReader reader = createReader(injectedValues);
+ return reader.withValueToUpdate(instance).readValue(json);
+ } catch (JsonProcessingException e) {
+ throw new GitHubJacksonException("Failed to deserialize JSON", e);
+ }
+ }
+
+ @Override
+ @Nonnull
+ public byte[] writeValueAsBytes(@Nonnull Object value) throws IOException {
+ try {
+ ObjectWriter writer = MAPPER.writer();
+ return writer.writeValueAsBytes(value);
+ } catch (JsonProcessingException e) {
+ throw new GitHubJacksonException("Failed to serialize object to JSON", e);
+ }
+ }
+
+ private ObjectReader createReader(@CheckForNull Map injectedValues) {
+ if (injectedValues == null || injectedValues.isEmpty()) {
+ Map defaultValues = new HashMap<>();
+ defaultValues.put(GitHubConnectorResponse.class.getName(), null);
+ defaultValues.put(GitHub.class.getName(), null);
+ return MAPPER.reader(new InjectableValues.Std(defaultValues));
+ }
+ return MAPPER.reader(new InjectableValues.Std(injectedValues));
+ }
+}
diff --git a/src/main/java/org/kohsuke/github/internal/GitHubJackson3.java b/src/main/java/org/kohsuke/github/internal/GitHubJackson3.java
new file mode 100644
index 0000000000..47c096119b
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/internal/GitHubJackson3.java
@@ -0,0 +1,218 @@
+package org.kohsuke.github.internal;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
+import org.kohsuke.github.GitHub;
+import org.kohsuke.github.GitHubRequest;
+import org.kohsuke.github.connector.GitHubConnectorRequest;
+import org.kohsuke.github.connector.GitHubConnectorResponse;
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.Version;
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.InjectableValues;
+import tools.jackson.databind.JsonNode;
+import tools.jackson.databind.MapperFeature;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.ObjectWriter;
+import tools.jackson.databind.PropertyNamingStrategies;
+import tools.jackson.databind.json.JsonMapper;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+
+/**
+ * Jackson 3.x implementation of {@link GitHubJackson}.
+ *
+ *
+ * This implementation uses Jackson 3.x APIs ({@code tools.jackson.*}).
+ *
+ *
+ *
+ * To use Jackson 3.x, add the {@code tools.jackson.core:jackson-databind} dependency to your project.
+ *
+ *
+ *
+ * Then configure the GitHub client to use Jackson 3:
+ *
+ *
+ *
+ * GitHub github = new GitHubBuilder().withJackson(DefaultGitHubJackson.createJackson3()).build();
+ *
+ *
+ * @author Pierre Villard
+ */
+public class GitHubJackson3 implements GitHubJackson {
+
+ private static final String JACKSON3_MARKER_CLASS = "tools.jackson.databind.json.JsonMapper";
+
+ private static final JsonMapper MAPPER = JsonMapper.builder()
+ // Java 8 date/time support is built-in to Jackson 3.x (no module needed)
+ .changeDefaultVisibility(vc -> vc.withFieldVisibility(Visibility.ANY)
+ .withGetterVisibility(Visibility.NONE)
+ .withSetterVisibility(Visibility.NONE)
+ .withCreatorVisibility(Visibility.NONE)
+ .withIsGetterVisibility(Visibility.NONE))
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
+ .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
+ .build();
+
+ /**
+ * Checks if Jackson 3.x is available on the classpath.
+ *
+ * @return true if Jackson 3.x classes can be loaded
+ */
+ public static boolean isAvailable() {
+ try {
+ Class.forName(JACKSON3_MARKER_CLASS);
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+
+ private final String version;
+
+ /**
+ * Creates a new GitHubJackson3 instance.
+ */
+ public GitHubJackson3() {
+ Version jacksonVersion = MAPPER.version();
+ this.version = "Jackson " + jacksonVersion.getMajorVersion() + "." + jacksonVersion.getMinorVersion() + "."
+ + jacksonVersion.getPatchLevel();
+ }
+
+ /**
+ * Adds a value to the injectable values map for the GitHub root object.
+ *
+ * @param injectedValues
+ * the map to modify
+ * @param root
+ * the GitHub root object to add
+ */
+ public void addGitHubRoot(@Nonnull Map injectedValues, @Nonnull GitHub root) {
+ injectedValues.put(GitHub.class.getName(), root);
+ }
+
+ @Override
+ @Nonnull
+ public Map createInjectableValues(@CheckForNull GitHubConnectorResponse connectorResponse) {
+ Map injected = new HashMap<>();
+
+ // Required or many things break
+ injected.put(GitHubConnectorResponse.class.getName(), null);
+ injected.put(GitHub.class.getName(), null);
+
+ if (connectorResponse != null) {
+ injected.put(GitHubConnectorResponse.class.getName(), connectorResponse);
+ GitHubConnectorRequest request = connectorResponse.request();
+ // GitHubRequest has additional injectable values
+ if (request instanceof GitHubRequest) {
+ injected.putAll(((GitHubRequest) request).injectedMappingValues());
+ }
+ }
+ return injected;
+ }
+
+ @Override
+ @Nonnull
+ public String getImplementationName() {
+ return version;
+ }
+
+ /**
+ * Gets an ObjectReader configured with injectable values.
+ *
+ *
+ * This method is exposed for compatibility with code that still needs direct access to ObjectReader.
+ *
+ *
+ * @param injectedValues
+ * values to inject during deserialization
+ * @return a configured ObjectReader
+ */
+ @Nonnull
+ public ObjectReader getReader(@CheckForNull Map injectedValues) {
+ return createReader(injectedValues);
+ }
+
+ /**
+ * Gets an ObjectWriter.
+ *
+ *
+ * This method is exposed for compatibility with code that still needs direct access to ObjectWriter.
+ *
+ *
+ * @return an ObjectWriter
+ */
+ @Nonnull
+ public ObjectWriter getWriter() {
+ return MAPPER.writer();
+ }
+
+ @Override
+ @CheckForNull
+ public T readValue(@Nonnull String json,
+ @Nonnull Class type,
+ @CheckForNull Map injectedValues) throws IOException {
+ try {
+ ObjectReader reader = createReader(injectedValues);
+ return reader.forType(type).readValue(json);
+ } catch (JacksonException e) {
+ throw new GitHubJacksonException("Failed to deserialize JSON", e);
+ }
+ }
+
+ @Override
+ @CheckForNull
+ public T readValueFromNode(@Nonnull Object node,
+ @Nonnull Class type,
+ @CheckForNull Map injectedValues) throws IOException {
+ if (!(node instanceof JsonNode)) {
+ throw new IllegalArgumentException("Node must be a Jackson 3.x JsonNode");
+ }
+ try {
+ ObjectReader reader = createReader(injectedValues);
+ return reader.forType(type).readValue((JsonNode) node);
+ } catch (JacksonException e) {
+ throw new GitHubJacksonException("Failed to deserialize JSON from node", e);
+ }
+ }
+
+ @Override
+ @CheckForNull
+ public T readValueToUpdate(@Nonnull String json,
+ @Nonnull T instance,
+ @CheckForNull Map injectedValues) throws IOException {
+ try {
+ ObjectReader reader = createReader(injectedValues);
+ return reader.withValueToUpdate(instance).readValue(json);
+ } catch (JacksonException e) {
+ throw new GitHubJacksonException("Failed to deserialize JSON", e);
+ }
+ }
+
+ @Override
+ @Nonnull
+ public byte[] writeValueAsBytes(@Nonnull Object value) throws IOException {
+ try {
+ ObjectWriter writer = MAPPER.writer();
+ return writer.writeValueAsBytes(value);
+ } catch (JacksonException e) {
+ throw new GitHubJacksonException("Failed to serialize object to JSON", e);
+ }
+ }
+
+ private ObjectReader createReader(@CheckForNull Map injectedValues) {
+ if (injectedValues == null || injectedValues.isEmpty()) {
+ Map defaultValues = new HashMap<>();
+ defaultValues.put(GitHubConnectorResponse.class.getName(), null);
+ defaultValues.put(GitHub.class.getName(), null);
+ return MAPPER.reader(new InjectableValues.Std(defaultValues));
+ }
+ return MAPPER.reader(new InjectableValues.Std(injectedValues));
+ }
+}
diff --git a/src/main/java/org/kohsuke/github/internal/GitHubJacksonException.java b/src/main/java/org/kohsuke/github/internal/GitHubJacksonException.java
new file mode 100644
index 0000000000..b77976531e
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/internal/GitHubJacksonException.java
@@ -0,0 +1,50 @@
+package org.kohsuke.github.internal;
+
+import java.io.IOException;
+
+/**
+ * Wrapper exception for Jackson-specific exceptions.
+ *
+ *
+ * This exception wraps Jackson-specific exceptions (from either Jackson 2.x or 3.x) to provide a consistent exception
+ * type that doesn't expose Jackson version-specific classes to callers.
+ *
+ *
+ * @author Pierre Villard
+ */
+public class GitHubJacksonException extends IOException {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new GitHubJacksonException with the specified detail message.
+ *
+ * @param message
+ * the detail message
+ */
+ public GitHubJacksonException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new GitHubJacksonException with the specified detail message and cause.
+ *
+ * @param message
+ * the detail message
+ * @param cause
+ * the cause (a Jackson-specific exception)
+ */
+ public GitHubJacksonException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructs a new GitHubJacksonException with the specified cause.
+ *
+ * @param cause
+ * the cause (a Jackson-specific exception)
+ */
+ public GitHubJacksonException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/test/java/org/kohsuke/github/GitHubJacksonTest.java b/src/test/java/org/kohsuke/github/GitHubJacksonTest.java
new file mode 100644
index 0000000000..ae35a8e199
--- /dev/null
+++ b/src/test/java/org/kohsuke/github/GitHubJacksonTest.java
@@ -0,0 +1,63 @@
+package org.kohsuke.github;
+
+import org.junit.Test;
+import org.kohsuke.github.internal.DefaultGitHubJackson;
+import org.kohsuke.github.internal.GitHubJackson;
+
+import java.io.IOException;
+
+import static org.hamcrest.CoreMatchers.*;
+
+/**
+ * Tests for Jackson implementation selection via {@link GitHubBuilder#withJackson(GitHubJackson)}.
+ */
+public class GitHubJacksonTest extends AbstractGitHubWireMockTest {
+
+ /**
+ * Create default GitHubJacksonTest instance.
+ */
+ public GitHubJacksonTest() {
+ useDefaultGitHub = false;
+ }
+
+ /**
+ * Test that the default Jackson implementation is Jackson 2.
+ *
+ * @throws IOException
+ * the io exception
+ */
+ @Test
+ public void testDefaultJacksonIsJackson2() throws IOException {
+ gitHub = getGitHubBuilder().build();
+ String implementationName = gitHub.getClient().getJacksonImplementationName();
+ assertThat(implementationName, startsWith("Jackson 2."));
+ }
+
+ /**
+ * Test that Jackson 2 can be explicitly configured via builder.
+ *
+ * @throws IOException
+ * the io exception
+ */
+ @Test
+ public void testJackson2ViaBuilder() throws IOException {
+ gitHub = getGitHubBuilder().withJackson(DefaultGitHubJackson.createJackson2()).build();
+ String implementationName = gitHub.getClient().getJacksonImplementationName();
+ assertThat(implementationName, startsWith("Jackson 2."));
+ }
+
+ /**
+ * Test that Jackson 3 can be configured via builder when available.
+ *
+ * @throws IOException
+ * the io exception
+ */
+ @Test
+ public void testJackson3ViaBuilder() throws IOException {
+ if (DefaultGitHubJackson.isJackson3Available()) {
+ gitHub = getGitHubBuilder().withJackson(DefaultGitHubJackson.createJackson3()).build();
+ String implementationName = gitHub.getClient().getJacksonImplementationName();
+ assertThat(implementationName, startsWith("Jackson 3."));
+ }
+ }
+}
diff --git a/src/test/java/org/kohsuke/github/internal/DefaultGitHubJacksonTest.java b/src/test/java/org/kohsuke/github/internal/DefaultGitHubJacksonTest.java
new file mode 100644
index 0000000000..53ed5dd7b6
--- /dev/null
+++ b/src/test/java/org/kohsuke/github/internal/DefaultGitHubJacksonTest.java
@@ -0,0 +1,62 @@
+package org.kohsuke.github.internal;
+
+import org.junit.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+
+/**
+ * Tests for {@link DefaultGitHubJackson} factory.
+ */
+public class DefaultGitHubJacksonTest {
+
+ /**
+ * Create default DefaultGitHubJacksonTest instance.
+ */
+ public DefaultGitHubJacksonTest() {
+ }
+
+ /**
+ * Test that createDefault returns Jackson 2 implementation.
+ */
+ @Test
+ public void testCreateDefault() {
+ GitHubJackson jackson = DefaultGitHubJackson.createDefault();
+ assertThat(jackson, notNullValue());
+ assertThat(jackson, instanceOf(GitHubJackson2.class));
+ assertThat(jackson.getImplementationName(), startsWith("Jackson 2."));
+ }
+
+ /**
+ * Test that createJackson2 returns Jackson 2 implementation.
+ */
+ @Test
+ public void testCreateJackson2() {
+ GitHubJackson2 jackson = DefaultGitHubJackson.createJackson2();
+ assertThat(jackson, notNullValue());
+ assertThat(jackson.getImplementationName(), startsWith("Jackson 2."));
+ }
+
+ /**
+ * Test that createJackson3 returns Jackson 3 implementation when available.
+ */
+ @Test
+ public void testCreateJackson3WhenAvailable() {
+ if (DefaultGitHubJackson.isJackson3Available()) {
+ GitHubJackson3 jackson = DefaultGitHubJackson.createJackson3();
+ assertThat(jackson, notNullValue());
+ assertThat(jackson.getImplementationName(), startsWith("Jackson 3."));
+ }
+ }
+
+ /**
+ * Test Jackson 3 availability check.
+ */
+ @Test
+ public void testJackson3Availability() {
+ // Since Jackson 3 is now on the classpath (as optional dependency),
+ // it should be available
+ boolean available = DefaultGitHubJackson.isJackson3Available();
+ assertThat(available, is(true));
+ }
+}
diff --git a/src/test/resources/no-reflect-and-serialization-list b/src/test/resources/no-reflect-and-serialization-list
index 4ad893272c..54d1f2b156 100644
--- a/src/test/resources/no-reflect-and-serialization-list
+++ b/src/test/resources/no-reflect-and-serialization-list
@@ -79,7 +79,12 @@ org.kohsuke.github.function.FunctionThrows
org.kohsuke.github.function.InputStreamFunction
org.kohsuke.github.function.SupplierThrows
org.kohsuke.github.internal.DefaultGitHubConnector
+org.kohsuke.github.internal.DefaultGitHubJackson
org.kohsuke.github.internal.EnumUtils
+org.kohsuke.github.internal.GitHubJackson
+org.kohsuke.github.internal.GitHubJackson2
+org.kohsuke.github.internal.GitHubJackson3
+org.kohsuke.github.internal.GitHubJacksonException
org.kohsuke.github.internal.Previews
org.kohsuke.github.EnterpriseManagedSupport
org.kohsuke.github.GHAutolinkBuilder
From 068dd6df54532d0469d91ece22774cd7ce684bbd Mon Sep 17 00:00:00 2001
From: Pierre Villard
Date: Mon, 26 Jan 2026 23:00:22 +0100
Subject: [PATCH 2/2] review
---
.../org/kohsuke/github/GitHubBuilder.java | 59 +++++++++----------
.../github/internal/DefaultGitHubJackson.java | 4 +-
.../github/internal/GitHubJackson.java | 4 +-
.../github/internal/GitHubJackson3.java | 2 +-
.../org/kohsuke/github/GitHubJacksonTest.java | 18 +-----
5 files changed, 33 insertions(+), 54 deletions(-)
diff --git a/src/main/java/org/kohsuke/github/GitHubBuilder.java b/src/main/java/org/kohsuke/github/GitHubBuilder.java
index 02b2e796a3..839ae4b45f 100644
--- a/src/main/java/org/kohsuke/github/GitHubBuilder.java
+++ b/src/main/java/org/kohsuke/github/GitHubBuilder.java
@@ -211,6 +211,33 @@ public GitHubBuilder clone() {
}
}
+ /**
+ * Configures the client to use Jackson 3.x for JSON serialization/deserialization.
+ *
+ *
+ * By default, Jackson 2.x is used. Call this method to use Jackson 3.x instead.
+ *
+ *
+ * Example
+ *
+ *
+ * GitHub github = new GitHubBuilder().withOAuthToken("token").useJackson3().build();
+ *
+ *
+ *
+ * Note: To use Jackson 3.x, you must add the Jackson 3 {@code tools.jackson.core:jackson-databind}
+ * dependency to your project.
+ *
+ *
+ * @return the GitHubBuilder
+ * @throws IllegalStateException
+ * if Jackson 3.x is not available on the classpath
+ */
+ public GitHubBuilder useJackson3() {
+ this.jackson = DefaultGitHubJackson.createJackson3();
+ return this;
+ }
+
/**
* Adds a {@link GitHubAbuseLimitHandler} to this {@link GitHubBuilder}.
*
@@ -282,38 +309,6 @@ public GitHubBuilder withEndpoint(String endpoint) {
return this;
}
- /**
- * Configures which Jackson implementation to use for JSON serialization/deserialization.
- *
- *
- * By default, Jackson 2.x is used. To use Jackson 3.x, create a Jackson 3 instance using
- * {@link DefaultGitHubJackson#createJackson3()} and pass it to this method.
- *
- *
- * Example: Using Jackson 3.x
- *
- *
- * GitHub github = new GitHubBuilder().withOAuthToken("token")
- * .withJackson(DefaultGitHubJackson.createJackson3())
- * .build();
- *
- *
- *
- * Note: To use Jackson 3.x, you must add the Jackson 3 {@code tools.jackson.core:jackson-databind}
- * dependency to your project.
- *
- *
- * @param jackson
- * the Jackson implementation to use
- * @return the GitHubBuilder
- * @see DefaultGitHubJackson#createJackson2()
- * @see DefaultGitHubJackson#createJackson3()
- */
- public GitHubBuilder withJackson(GitHubJackson jackson) {
- this.jackson = jackson;
- return this;
- }
-
/**
* With jwt token GitHubBuilder.
*
diff --git a/src/main/java/org/kohsuke/github/internal/DefaultGitHubJackson.java b/src/main/java/org/kohsuke/github/internal/DefaultGitHubJackson.java
index 1742116cf1..ea88399d94 100644
--- a/src/main/java/org/kohsuke/github/internal/DefaultGitHubJackson.java
+++ b/src/main/java/org/kohsuke/github/internal/DefaultGitHubJackson.java
@@ -19,9 +19,7 @@
* GitHub github = new GitHubBuilder().withOAuthToken("token").build();
*
* // Using Jackson 3.x
- * GitHub github = new GitHubBuilder().withOAuthToken("token")
- * .withJackson(DefaultGitHubJackson.createJackson3())
- * .build();
+ * GitHub github = new GitHubBuilder().withOAuthToken("token").useJackson3().build();
*
*
* Jackson 3.x Dependencies
diff --git a/src/main/java/org/kohsuke/github/internal/GitHubJackson.java b/src/main/java/org/kohsuke/github/internal/GitHubJackson.java
index eab9290beb..b2e63ecfe8 100644
--- a/src/main/java/org/kohsuke/github/internal/GitHubJackson.java
+++ b/src/main/java/org/kohsuke/github/internal/GitHubJackson.java
@@ -24,12 +24,12 @@
*
* Configuration
*
- * Use {@link org.kohsuke.github.GitHubBuilder#withJackson(GitHubJackson)} to configure which Jackson version to use:
+ * Use {@link org.kohsuke.github.GitHubBuilder#useJackson3()} to configure the client to use Jackson 3.x:
*
*
*
* // Use Jackson 3.x
- * GitHub github = new GitHubBuilder().withJackson(DefaultGitHubJackson.createJackson3()).build();
+ * GitHub github = new GitHubBuilder().useJackson3().build();
*
*
* @author Pierre Villard
diff --git a/src/main/java/org/kohsuke/github/internal/GitHubJackson3.java b/src/main/java/org/kohsuke/github/internal/GitHubJackson3.java
index 47c096119b..e863513294 100644
--- a/src/main/java/org/kohsuke/github/internal/GitHubJackson3.java
+++ b/src/main/java/org/kohsuke/github/internal/GitHubJackson3.java
@@ -39,7 +39,7 @@
*
*
*
- * GitHub github = new GitHubBuilder().withJackson(DefaultGitHubJackson.createJackson3()).build();
+ * GitHub github = new GitHubBuilder().useJackson3().build();
*
*
* @author Pierre Villard
diff --git a/src/test/java/org/kohsuke/github/GitHubJacksonTest.java b/src/test/java/org/kohsuke/github/GitHubJacksonTest.java
index ae35a8e199..dd7376f3de 100644
--- a/src/test/java/org/kohsuke/github/GitHubJacksonTest.java
+++ b/src/test/java/org/kohsuke/github/GitHubJacksonTest.java
@@ -2,14 +2,13 @@
import org.junit.Test;
import org.kohsuke.github.internal.DefaultGitHubJackson;
-import org.kohsuke.github.internal.GitHubJackson;
import java.io.IOException;
import static org.hamcrest.CoreMatchers.*;
/**
- * Tests for Jackson implementation selection via {@link GitHubBuilder#withJackson(GitHubJackson)}.
+ * Tests for Jackson implementation selection via {@link GitHubBuilder#useJackson3()}.
*/
public class GitHubJacksonTest extends AbstractGitHubWireMockTest {
@@ -33,19 +32,6 @@ public void testDefaultJacksonIsJackson2() throws IOException {
assertThat(implementationName, startsWith("Jackson 2."));
}
- /**
- * Test that Jackson 2 can be explicitly configured via builder.
- *
- * @throws IOException
- * the io exception
- */
- @Test
- public void testJackson2ViaBuilder() throws IOException {
- gitHub = getGitHubBuilder().withJackson(DefaultGitHubJackson.createJackson2()).build();
- String implementationName = gitHub.getClient().getJacksonImplementationName();
- assertThat(implementationName, startsWith("Jackson 2."));
- }
-
/**
* Test that Jackson 3 can be configured via builder when available.
*
@@ -55,7 +41,7 @@ public void testJackson2ViaBuilder() throws IOException {
@Test
public void testJackson3ViaBuilder() throws IOException {
if (DefaultGitHubJackson.isJackson3Available()) {
- gitHub = getGitHubBuilder().withJackson(DefaultGitHubJackson.createJackson3()).build();
+ gitHub = getGitHubBuilder().useJackson3().build();
String implementationName = gitHub.getClient().getJacksonImplementationName();
assertThat(implementationName, startsWith("Jackson 3."));
}