From d5a871e7ee454ca9079863ca17e2d594c7b8df45 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 26 Nov 2025 11:52:10 +0000 Subject: [PATCH 1/2] Improve Jackson configuration for the actuator endpoints to include package private constructors. Jackson 3 does not detected package private constructors anyore in case they have more than one argument. This prevents the Restclient / Resttemplate and their test derivates deserializing metrics endpoints as they could do in Spring Boot 3.x with Jackson 2. This changes the isolated mapper to the behaviour of Jackson 2, so that the mapping works out of the box again. Signed-off-by: Michael Simons --- .../JacksonEndpointAutoConfiguration.java | 2 + ...JacksonEndpointAutoConfigurationTests.java | 20 ++++++++ .../jackson/example/SomeResponseBody.java | 49 +++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 module/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/example/SomeResponseBody.java diff --git a/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfiguration.java b/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfiguration.java index 276c3eaff099..72bc3f6bd7d7 100644 --- a/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfiguration.java +++ b/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfiguration.java @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.jackson; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; import com.fasterxml.jackson.annotation.JsonInclude.Include; import tools.jackson.databind.json.JsonMapper; @@ -42,6 +43,7 @@ EndpointJsonMapper endpointJsonMapper() { JsonMapper jsonMapper = JsonMapper.builder() .changeDefaultPropertyInclusion( (value) -> value.withValueInclusion(Include.NON_NULL).withContentInclusion(Include.NON_NULL)) + .changeDefaultVisibility(vc -> vc.withCreatorVisibility(Visibility.NON_PRIVATE)) .build(); return () -> jsonMapper; } diff --git a/module/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfigurationTests.java b/module/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfigurationTests.java index 6e8ad5a0d775..173a8b00d3e8 100644 --- a/module/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfigurationTests.java +++ b/module/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfigurationTests.java @@ -25,7 +25,10 @@ import org.junit.jupiter.api.Test; import tools.jackson.databind.json.JsonMapper; +import org.springframework.boot.actuate.autoconfigure.endpoint.jackson.example.SomeResponseBody; +import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.jackson.EndpointJsonMapper; +import org.springframework.boot.actuate.web.mappings.MappingsEndpoint.ApplicationMappingsDescriptor; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -91,6 +94,23 @@ void endpointJsonMapperDoesNotSerializeNullValues() { }); } + @Test + void endpointShouldDeserializeOperationResponseBodies() { + this.runner.run((context) -> { + JsonMapper jsonMapper = context.getBean(EndpointJsonMapper.class).get(); + String json = """ + { + "value1": "a", + "value2": "b", + } + """; + SomeResponseBody responseBody = jsonMapper.readValue(json, SomeResponseBody.class); + assertThat(responseBody.getValue1()).isEqualTo("a"); + assertThat(responseBody.getValue2()).isEqualTo("b"); + }); + + } + @Configuration(proxyBeanMethods = false) static class TestEndpointMapperConfiguration { diff --git a/module/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/example/SomeResponseBody.java b/module/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/example/SomeResponseBody.java new file mode 100644 index 000000000000..9347a81c2c72 --- /dev/null +++ b/module/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/example/SomeResponseBody.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.endpoint.jackson.example; + +import org.springframework.boot.actuate.endpoint.OperationResponseBody; + +/** + * A class that reassembles something like + * {@code org.springframework.boot.micrometer.metrics.actuate.endpoint.MetricsEndpoint.MetricDescriptor}, + * not exposing a public constructor, only package level. They could be deserialized with + * Jackson 2 out of the box, but not with Jackson 3 anymore. Constructors with one + * argument still deserialize as is, so this need to have two or more. + * + * @author Michael J. Simons + */ +public final class SomeResponseBody implements OperationResponseBody { + + private final String value1; + + private final String value2; + + SomeResponseBody(String value1, String value2) { + this.value1 = value1; + this.value2 = value2; + } + + public String getValue1() { + return this.value1; + } + + public String getValue2() { + return this.value2; + } + +} From 98d372f858fa73fd29aaf0ad9a801905a6e13765 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 26 Nov 2025 12:24:25 +0000 Subject: [PATCH 2/2] Fix formatting. --- .../endpoint/jackson/JacksonEndpointAutoConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfiguration.java b/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfiguration.java index 72bc3f6bd7d7..ec4e04be5858 100644 --- a/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfiguration.java +++ b/module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfiguration.java @@ -43,7 +43,7 @@ EndpointJsonMapper endpointJsonMapper() { JsonMapper jsonMapper = JsonMapper.builder() .changeDefaultPropertyInclusion( (value) -> value.withValueInclusion(Include.NON_NULL).withContentInclusion(Include.NON_NULL)) - .changeDefaultVisibility(vc -> vc.withCreatorVisibility(Visibility.NON_PRIVATE)) + .changeDefaultVisibility((vc) -> vc.withCreatorVisibility(Visibility.NON_PRIVATE)) .build(); return () -> jsonMapper; }