diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfiguration.java index 40565258873..14ec5bfccf4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfiguration.java @@ -16,7 +16,9 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.jackson; +import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import org.springframework.boot.actuate.endpoint.jackson.EndpointObjectMapper; import org.springframework.boot.autoconfigure.AutoConfigureAfter; @@ -42,7 +44,10 @@ public class JacksonEndpointAutoConfiguration { @ConditionalOnProperty(name = "management.endpoints.jackson.isolated-object-mapper", matchIfMissing = true) @ConditionalOnClass({ ObjectMapper.class, Jackson2ObjectMapperBuilder.class }) public EndpointObjectMapper endpointObjectMapper() { - ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().build(); + ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json() + .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, + SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) + .serializationInclusion(Include.NON_NULL).build(); return () -> objectMapper; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfigurationTests.java index 2d76e97fe19..f4b1fa5ddcf 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfigurationTests.java @@ -16,6 +16,12 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.jackson; +import java.time.Duration; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; + import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; @@ -54,6 +60,37 @@ class JacksonEndpointAutoConfigurationTests { .run((context) -> assertThat(context).doesNotHaveBean(EndpointObjectMapper.class)); } + @Test + void endpointObjectMapperDoesNotSerializeDatesAsTimestamps() { + this.runner.run((context) -> { + ObjectMapper objectMapper = context.getBean(EndpointObjectMapper.class).get(); + Instant now = Instant.now(); + String json = objectMapper.writeValueAsString(Map.of("timestamp", now)); + assertThat(json).contains(DateTimeFormatter.ISO_INSTANT.format(now)); + }); + } + + @Test + void endpointObjectMapperDoesNotSerializeDurationsAsTimestamps() { + this.runner.run((context) -> { + ObjectMapper objectMapper = context.getBean(EndpointObjectMapper.class).get(); + Duration duration = Duration.ofSeconds(42); + String json = objectMapper.writeValueAsString(Map.of("duration", duration)); + assertThat(json).contains(duration.toString()); + }); + } + + @Test + void endpointObjectMapperDoesNotSerializeNullValues() { + this.runner.run((context) -> { + ObjectMapper objectMapper = context.getBean(EndpointObjectMapper.class).get(); + HashMap map = new HashMap<>(); + map.put("key", null); + String json = objectMapper.writeValueAsString(map); + assertThat(json).isEqualTo("{}"); + }); + } + @Configuration(proxyBeanMethods = false) static class TestEndpointMapperConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/AbstractEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/AbstractEndpointDocumentationTests.java index 57c1ae0e501..e3eebbf8c40 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/AbstractEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/AbstractEndpointDocumentationTests.java @@ -26,10 +26,14 @@ import java.util.stream.Stream; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.jackson.JacksonEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.web.reactive.WebFluxEndpointManagementContextConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet.WebMvcEndpointManagementContextConfiguration; +import org.springframework.boot.actuate.endpoint.jackson.EndpointObjectMapper; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; @@ -38,6 +42,7 @@ import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfig import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.restdocs.operation.preprocess.ContentModifyingOperationPreprocessor; import org.springframework.restdocs.operation.preprocess.OperationPreprocessor; @@ -54,8 +59,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWit * * @author Andy Wilkinson */ -@TestPropertySource(properties = { "spring.jackson.serialization.indent_output=true", - "management.endpoints.web.exposure.include=*", "spring.jackson.default-property-inclusion=non_null" }) +@TestPropertySource(properties = { "management.endpoints.web.exposure.include=*" }) public abstract class AbstractEndpointDocumentationTests { protected static String describeEnumValues(Class> enumType) { @@ -119,9 +123,26 @@ public abstract class AbstractEndpointDocumentationTests { WebMvcAutoConfiguration.class, DispatcherServletAutoConfiguration.class, EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, WebMvcEndpointManagementContextConfiguration.class, WebFluxEndpointManagementContextConfiguration.class, PropertyPlaceholderAutoConfiguration.class, - WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class }) + WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, + JacksonEndpointAutoConfiguration.class }) static class BaseDocumentationConfiguration { + @Bean + static BeanPostProcessor endpointObjectMapperBeanPostProcessor() { + return new BeanPostProcessor() { + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof EndpointObjectMapper) { + return (EndpointObjectMapper) () -> ((EndpointObjectMapper) bean).get() + .enable(SerializationFeature.INDENT_OUTPUT); + } + return bean; + } + + }; + } + } }