From b1a8d76ef76c110549ec76a22919eddd2a676c90 Mon Sep 17 00:00:00 2001 From: Pierre Villard Date: Sun, 21 Dec 2025 15:40:36 +0100 Subject: [PATCH] Move to Jackson 3 --- pom.xml | 32 ++++++++------- .../github/EnterpriseManagedSupport.java | 6 +-- .../java/org/kohsuke/github/GHEventInfo.java | 4 +- src/main/java/org/kohsuke/github/GHRef.java | 16 +++++++- .../org/kohsuke/github/GHRepositoryRule.java | 4 +- .../github/GHRepositoryStatistics.java | 2 +- src/main/java/org/kohsuke/github/GitHub.java | 4 +- .../java/org/kohsuke/github/GitHubClient.java | 41 +++++++++++++------ .../org/kohsuke/github/GitHubResponse.java | 13 +++--- .../kohsuke/github/AotIntegrationTest.java | 10 +++-- .../org/kohsuke/github/GHRateLimitTest.java | 16 +++++--- .../org/kohsuke/github/GHRepositoryTest.java | 6 ++- .../GitHubConnectorResponseTest.java | 2 +- .../response/GHGraphQLResponseMockTest.java | 25 ++++++----- 14 files changed, 109 insertions(+), 72 deletions(-) diff --git a/pom.xml b/pom.xml index 041d55bc77..6e0f50c4f8 100644 --- a/pom.xml +++ b/pom.xml @@ -86,13 +86,6 @@ - - com.fasterxml.jackson - jackson-bom - 2.20.0 - pom - import - org.junit junit-bom @@ -114,6 +107,19 @@ pom import + + tools.jackson + jackson-bom + 3.0.3 + pom + import + + + + com.fasterxml.jackson.core + jackson-annotations + 2.20 + junit junit @@ -138,14 +144,6 @@ - - com.fasterxml.jackson.core - jackson-databind - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - com.github.spotbugs spotbugs-annotations @@ -196,6 +194,10 @@ commons-lang3 3.19.0 + + tools.jackson.core + jackson-databind + com.github.npathai hamcrest-optional diff --git a/src/main/java/org/kohsuke/github/EnterpriseManagedSupport.java b/src/main/java/org/kohsuke/github/EnterpriseManagedSupport.java index 9d3030558d..db73aae935 100644 --- a/src/main/java/org/kohsuke/github/EnterpriseManagedSupport.java +++ b/src/main/java/org/kohsuke/github/EnterpriseManagedSupport.java @@ -1,6 +1,6 @@ package org.kohsuke.github; -import com.fasterxml.jackson.core.JsonProcessingException; +import tools.jackson.core.JacksonException; import java.io.PrintWriter; import java.io.StringWriter; @@ -20,7 +20,7 @@ class EnterpriseManagedSupport { static final String TEAM_CANNOT_BE_EXTERNALLY_MANAGED_ERROR = "This team cannot be externally managed since it has explicit members."; - private static String logUnexpectedFailure(final JsonProcessingException exception, final String payload) { + private static String logUnexpectedFailure(final JacksonException exception, final String payload) { final StringWriter sw = new StringWriter(); final PrintWriter pw = new PrintWriter(sw); exception.printStackTrace(pw); @@ -58,7 +58,7 @@ Optional filterException(final HttpException he, final String sce } else if (TEAM_CANNOT_BE_EXTERNALLY_MANAGED_ERROR.equals(error.getMessage())) { return Optional.of(new GHTeamCannotBeExternallyManagedException(scenario, error, he)); } - } catch (final JsonProcessingException e) { + } catch (final JacksonException e) { // We can ignore it LOGGER.warning(() -> logUnexpectedFailure(e, responseMessage)); } diff --git a/src/main/java/org/kohsuke/github/GHEventInfo.java b/src/main/java/org/kohsuke/github/GHEventInfo.java index b9adccf2e6..87030f478a 100644 --- a/src/main/java/org/kohsuke/github/GHEventInfo.java +++ b/src/main/java/org/kohsuke/github/GHEventInfo.java @@ -1,8 +1,8 @@ package org.kohsuke.github; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import tools.jackson.databind.node.ObjectNode; import java.io.IOException; import java.time.Instant; @@ -174,7 +174,7 @@ public GHOrganization getOrganization() throws IOException { * if payload cannot be parsed */ public T getPayload(Class type) throws IOException { - T v = GitHubClient.getMappingObjectReader(root()).readValue(payload.traverse(), type); + T v = GitHubClient.getMappingObjectReader(root()).forType(type).readValue(payload); v.lateBind(); return v; } diff --git a/src/main/java/org/kohsuke/github/GHRef.java b/src/main/java/org/kohsuke/github/GHRef.java index 33d54792db..1214f21c5c 100644 --- a/src/main/java/org/kohsuke/github/GHRef.java +++ b/src/main/java/org/kohsuke/github/GHRef.java @@ -1,7 +1,7 @@ package org.kohsuke.github; -import com.fasterxml.jackson.databind.JsonMappingException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import tools.jackson.databind.DatabindException; import java.io.IOException; import java.net.URL; @@ -59,6 +59,17 @@ public URL getUrl() { } } + private static boolean hasDatabindExceptionCause(Throwable e) { + Throwable cause = e; + while (cause != null) { + if (cause instanceof DatabindException) { + return true; + } + cause = cause.getCause(); + } + return false; + } + /** * Retrieve a ref of the given type for the current GitHub repository. * @@ -88,7 +99,8 @@ static GHRef read(GHRepository repository, String refName) throws IOException { // If the parse exception is due to the above returning an array instead of a single ref // that means the individual ref did not exist. Handled by result check below. // Otherwise, rethrow. - if (!(e.getCause() instanceof JsonMappingException)) { + // In Jackson 3, DatabindException is wrapped in IOException, so check the nested cause chain + if (!hasDatabindExceptionCause(e)) { throw e; } } diff --git a/src/main/java/org/kohsuke/github/GHRepositoryRule.java b/src/main/java/org/kohsuke/github/GHRepositoryRule.java index d348153e85..ebd5a09ace 100644 --- a/src/main/java/org/kohsuke/github/GHRepositoryRule.java +++ b/src/main/java/org/kohsuke/github/GHRepositoryRule.java @@ -1,9 +1,9 @@ package org.kohsuke.github; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.kohsuke.github.internal.EnumUtils; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.JsonNode; import java.io.IOException; import java.util.List; diff --git a/src/main/java/org/kohsuke/github/GHRepositoryStatistics.java b/src/main/java/org/kohsuke/github/GHRepositoryStatistics.java index ece1e9d87d..0eafc735b0 100644 --- a/src/main/java/org/kohsuke/github/GHRepositoryStatistics.java +++ b/src/main/java/org/kohsuke/github/GHRepositoryStatistics.java @@ -1,8 +1,8 @@ package org.kohsuke.github; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.exc.MismatchedInputException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import tools.jackson.databind.exc.MismatchedInputException; import java.io.IOException; import java.util.Arrays; diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java index cb47de47af..e4e2a5fb7f 100644 --- a/src/main/java/org/kohsuke/github/GitHub.java +++ b/src/main/java/org/kohsuke/github/GitHub.java @@ -23,13 +23,13 @@ */ package org.kohsuke.github; -import com.fasterxml.jackson.databind.ObjectReader; -import com.fasterxml.jackson.databind.ObjectWriter; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.kohsuke.github.authorization.AuthorizationProvider; import org.kohsuke.github.authorization.ImmutableAuthorizationProvider; import org.kohsuke.github.authorization.UserAuthorizationProvider; import org.kohsuke.github.connector.GitHubConnector; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.ObjectWriter; import java.io.*; import java.util.*; diff --git a/src/main/java/org/kohsuke/github/GitHubClient.java b/src/main/java/org/kohsuke/github/GitHubClient.java index 70eec892ec..a0a7603f99 100644 --- a/src/main/java/org/kohsuke/github/GitHubClient.java +++ b/src/main/java/org/kohsuke/github/GitHubClient.java @@ -1,8 +1,5 @@ package org.kohsuke.github; -import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.introspect.VisibilityChecker; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.apache.commons.io.IOUtils; import org.kohsuke.github.authorization.AuthorizationProvider; import org.kohsuke.github.authorization.UserAuthorizationProvider; @@ -10,6 +7,13 @@ import org.kohsuke.github.connector.GitHubConnectorRequest; import org.kohsuke.github.connector.GitHubConnectorResponse; import org.kohsuke.github.function.FunctionThrows; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.InjectableValues; +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.*; import java.net.*; @@ -109,21 +113,34 @@ static class RetryRequestException extends IOException { /** The Constant DEFAULT_MINIMUM_RETRY_TIMEOUT_MILLIS. */ private static final int DEFAULT_MINIMUM_RETRY_MILLIS = DEFAULT_MAXIMUM_RETRY_MILLIS; private static final Logger LOGGER = Logger.getLogger(GitHubClient.class.getName()); - private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final JsonMapper MAPPER = JsonMapper.builder() + // Use annotations and enable access to private fields + .enable(MapperFeature.USE_ANNOTATIONS) + .enable(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS) + .enable(MapperFeature.INFER_PROPERTY_MUTATORS) + .enable(MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS) + // Set visibility to detect all fields (including private fields) + // This matches the original Jackson 2 config: new VisibilityChecker.Std(NONE, NONE, NONE, NONE, ANY) + .changeDefaultVisibility(vc -> { + return vc.withGetterVisibility(NONE) + .withIsGetterVisibility(NONE) + .withSetterVisibility(NONE) + .withCreatorVisibility(NONE) + .withFieldVisibility(ANY); + }) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + // Jackson 3 enables these by default - disable to match Jackson 2.x behavior + .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) + .build(); private static final ThreadLocal sendRequestTraceId = new ThreadLocal<>(); /** The Constant GITHUB_URL. */ static final String GITHUB_URL = "https://api.github.com"; - static { - MAPPER.registerModule(new JavaTimeModule()); - MAPPER.setVisibility(new VisibilityChecker.Std(NONE, NONE, NONE, NONE, ANY)); - MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - MAPPER.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true); - MAPPER.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); - } - @Nonnull private static GitHubResponse createResponse(@Nonnull GitHubConnectorResponse connectorResponse, @CheckForNull BodyHandler handler) throws IOException { diff --git a/src/main/java/org/kohsuke/github/GitHubResponse.java b/src/main/java/org/kohsuke/github/GitHubResponse.java index 8ac65391f7..8e0bcf7746 100644 --- a/src/main/java/org/kohsuke/github/GitHubResponse.java +++ b/src/main/java/org/kohsuke/github/GitHubResponse.java @@ -1,10 +1,9 @@ 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; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.InjectableValues; import java.io.IOException; import java.io.InputStream; @@ -99,10 +98,10 @@ static T parseBody(GitHubConnectorResponse connectorResponse, Class type) inject.addValue(GitHubConnectorResponse.class, connectorResponse); return GitHubClient.getMappingObjectReader(connectorResponse).forType(type).readValue(data); - } catch (JsonMappingException | JsonParseException e) { + } catch (JacksonException e) { String message = "Failed to deserialize: " + data; LOGGER.log(Level.FINE, message); - throw e; + throw new IOException(message, e); } } @@ -125,10 +124,10 @@ static T parseBody(GitHubConnectorResponse connectorResponse, T instance) th String data = getBodyAsString(connectorResponse); try { return GitHubClient.getMappingObjectReader(connectorResponse).withValueToUpdate(instance).readValue(data); - } catch (JsonMappingException | JsonParseException e) { + } catch (JacksonException e) { String message = "Failed to deserialize: " + data; LOGGER.log(Level.FINE, message); - throw e; + throw new IOException(message, e); } } diff --git a/src/test/java/org/kohsuke/github/AotIntegrationTest.java b/src/test/java/org/kohsuke/github/AotIntegrationTest.java index a8b458f792..e1e28a348d 100644 --- a/src/test/java/org/kohsuke/github/AotIntegrationTest.java +++ b/src/test/java/org/kohsuke/github/AotIntegrationTest.java @@ -1,10 +1,10 @@ package org.kohsuke.github; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; import org.junit.Test; import org.springframework.boot.test.context.SpringBootTest; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.node.ArrayNode; import java.io.IOException; import java.nio.file.Files; @@ -80,7 +80,9 @@ public void testIfAllRequiredClassesAreRegisteredForAot() throws IOException { private Stream readAotConfigToStreamOfClassNames(String reflectionConfig) throws IOException { byte[] reflectionConfigFileAsBytes = Files.readAllBytes(Path.of(reflectionConfig)); - ArrayNode reflectConfigJsonArray = (ArrayNode) new ObjectMapper().readTree(reflectionConfigFileAsBytes); + ArrayNode reflectConfigJsonArray = (ArrayNode) JsonMapper.builder() + .build() + .readTree(reflectionConfigFileAsBytes); return StreamSupport .stream(Spliterators.spliteratorUnknownSize(reflectConfigJsonArray.iterator(), Spliterator.ORDERED), false) diff --git a/src/test/java/org/kohsuke/github/GHRateLimitTest.java b/src/test/java/org/kohsuke/github/GHRateLimitTest.java index 66fcc21df1..068dbd59ba 100644 --- a/src/test/java/org/kohsuke/github/GHRateLimitTest.java +++ b/src/test/java/org/kohsuke/github/GHRateLimitTest.java @@ -1,9 +1,9 @@ package org.kohsuke.github; -import com.fasterxml.jackson.databind.exc.MismatchedInputException; -import com.fasterxml.jackson.databind.exc.ValueInstantiationException; import com.github.tomakehurst.wiremock.core.WireMockConfiguration; import org.junit.Test; +import tools.jackson.databind.exc.MismatchedInputException; +import tools.jackson.databind.exc.ValueInstantiationException; import java.io.IOException; import java.time.Duration; @@ -440,8 +440,10 @@ public void testGitHubRateLimitWithBadData() throws Exception { fail("Invalid rate limit missing some records should throw"); } catch (Exception e) { assertThat(e, instanceOf(HttpException.class)); - assertThat(e.getCause(), instanceOf(ValueInstantiationException.class)); - assertThat(e.getCause().getMessage(), + // In Jackson 3, the exception is wrapped in IOException + assertThat(e.getCause(), instanceOf(IOException.class)); + assertThat(e.getCause().getCause(), instanceOf(ValueInstantiationException.class)); + assertThat(e.getCause().getCause().getMessage(), containsString( "Cannot construct instance of `org.kohsuke.github.GHRateLimit`, problem: `java.lang.NullPointerException`")); } @@ -451,8 +453,10 @@ public void testGitHubRateLimitWithBadData() throws Exception { fail("Invalid rate limit record missing a value should throw"); } catch (Exception e) { assertThat(e, instanceOf(HttpException.class)); - assertThat(e.getCause(), instanceOf(MismatchedInputException.class)); - assertThat(e.getCause().getMessage(), + // In Jackson 3, the exception is wrapped in IOException + assertThat(e.getCause(), instanceOf(IOException.class)); + assertThat(e.getCause().getCause(), instanceOf(MismatchedInputException.class)); + assertThat(e.getCause().getCause().getMessage(), containsString("Missing required creator property 'reset' (index 2)")); } diff --git a/src/test/java/org/kohsuke/github/GHRepositoryTest.java b/src/test/java/org/kohsuke/github/GHRepositoryTest.java index db5d892f85..63e1496cea 100644 --- a/src/test/java/org/kohsuke/github/GHRepositoryTest.java +++ b/src/test/java/org/kohsuke/github/GHRepositoryTest.java @@ -1,6 +1,5 @@ package org.kohsuke.github; -import com.fasterxml.jackson.databind.JsonMappingException; import com.google.common.collect.Sets; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.apache.commons.io.IOUtils; @@ -9,6 +8,7 @@ import org.kohsuke.github.GHCheckRun.Conclusion; import org.kohsuke.github.GHOrganization.RepositoryRole; import org.kohsuke.github.GHRepository.Visibility; +import tools.jackson.databind.DatabindException; import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; @@ -1039,7 +1039,9 @@ public void listRefs() throws Exception { fail(); } catch (Exception e) { assertThat(e, instanceOf(HttpException.class)); - assertThat(e.getCause(), instanceOf(JsonMappingException.class)); + // In Jackson 3, DatabindException is wrapped in IOException + assertThat(e.getCause(), instanceOf(IOException.class)); + assertThat(e.getCause().getCause(), instanceOf(DatabindException.class)); } // git/refs/heads/gh diff --git a/src/test/java/org/kohsuke/github/connector/GitHubConnectorResponseTest.java b/src/test/java/org/kohsuke/github/connector/GitHubConnectorResponseTest.java index daef1d758f..298d60cb57 100644 --- a/src/test/java/org/kohsuke/github/connector/GitHubConnectorResponseTest.java +++ b/src/test/java/org/kohsuke/github/connector/GitHubConnectorResponseTest.java @@ -1,6 +1,5 @@ package org.kohsuke.github.connector; -import com.fasterxml.jackson.databind.util.ByteBufferBackedInputStream; import org.apache.commons.io.IOUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -8,6 +7,7 @@ import org.junit.Test; import org.kohsuke.github.AbstractGitHubWireMockTest; import org.kohsuke.github.connector.GitHubConnectorResponse.ByteArrayResponse; +import tools.jackson.databind.util.ByteBufferBackedInputStream; import java.io.ByteArrayInputStream; import java.io.IOException; diff --git a/src/test/java/org/kohsuke/github/internal/graphql/response/GHGraphQLResponseMockTest.java b/src/test/java/org/kohsuke/github/internal/graphql/response/GHGraphQLResponseMockTest.java index a98870c6ec..0ae97db56a 100644 --- a/src/test/java/org/kohsuke/github/internal/graphql/response/GHGraphQLResponseMockTest.java +++ b/src/test/java/org/kohsuke/github/internal/graphql/response/GHGraphQLResponseMockTest.java @@ -1,11 +1,11 @@ package org.kohsuke.github.internal.graphql.response; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectReader; import org.junit.jupiter.api.Test; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.json.JsonMapper; import java.util.List; @@ -19,11 +19,10 @@ */ class GHGraphQLResponseMockTest { - private GHGraphQLResponse convertJsonToGraphQLResponse(String json) throws JsonProcessingException { - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + private GHGraphQLResponse convertJsonToGraphQLResponse(String json) throws JacksonException { + JsonMapper mapper = JsonMapper.builder().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).build(); - ObjectReader objectReader = objectMapper.reader(); + ObjectReader objectReader = mapper.reader(); JavaType javaType = objectReader.getTypeFactory() .constructParametricType(GHGraphQLResponse.class, Object.class); @@ -33,12 +32,12 @@ private GHGraphQLResponse convertJsonToGraphQLResponse(String json) thro /** * Test get data throws exception when response means error * - * @throws JsonProcessingException + * @throws JacksonException * Json parse exception * */ @Test - void getDataFailure() throws JsonProcessingException { + void getDataFailure() throws JacksonException { String graphQLErrorResponse = "{\"data\": {\"enablePullRequestAutoMerge\": null},\"errors\": [{\"type\": " + "\"UNPROCESSABLE\",\"path\": [\"enablePullRequestAutoMerge\"],\"locations\": [{\"line\": 2," + "\"column\": 5}],\"message\": \"hub4j does not have a verified email, which is required to enable " @@ -56,11 +55,11 @@ void getDataFailure() throws JsonProcessingException { /** * Test getErrorMessages throws exception when response means not error * - * @throws JsonProcessingException + * @throws JacksonException * Json parse exception */ @Test - void getErrorMessagesFailure() throws JsonProcessingException { + void getErrorMessagesFailure() throws JacksonException { String graphQLSuccessResponse = "{\"data\": {\"repository\": {\"pullRequest\": {\"id\": " + "\"PR_TEMP_GRAPHQL_ID\"}}}}";