In Spring Boot Actuator 2.7.8, the Health class got a new field exception. This was includes in commit d7852cb as part of #32527.

Unfortunately, Gson is not able to serialize exceptions out of the box in Java 17 due to the closed down module system. This leads to the following exception when calling the health actuator REST endpoint:

org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Failed making field 'java.lang.Throwable#detailMessage' accessible; either increase its visibility or write a custom TypeAdapter for its declaring type.; nested exception is com.google.gson.JsonIOException: Failed making field 'java.lang.Throwable#detailMessage' accessible; either increase its visibility or write a custom TypeAdapter for its declaring type.
    at org.springframework.http.converter.json.AbstractJsonHttpMessageConverter.writeInternal(AbstractJsonHttpMessageConverter.java:128) ~[spring-web-5.3.25.jar:5.3.25]
    at org.springframework.http.converter.AbstractGenericHttpMessageConverter.write(AbstractGenericHttpMessageConverter.java:104) ~[spring-web-5.3.25.jar:5.3.25]
    at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:290) ~[spring-webmvc-5.3.25.jar:5.3.25]
    at org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor.handleReturnValue(HttpEntityMethodProcessor.java:219) ~[spring-webmvc-5.3.25.jar:5.3.25]
    at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:78) ~[spring-web-5.3.25.jar:5.3.25]
    at ...
Caused by: com.google.gson.JsonIOException: Failed making field 'java.lang.Throwable#detailMessage' accessible; either increase its visibility or write a custom TypeAdapter for its declaring type.
    at com.google.gson.internal.reflect.ReflectionHelper.makeAccessible(ReflectionHelper.java:38) ~[gson-2.10.1.jar:?]
    at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.getBoundFields(ReflectiveTypeAdapterFactory.java:286) ~[gson-2.10.1.jar:?]
    at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.create(ReflectiveTypeAdapterFactory.java:130) ~[gson-2.10.1.jar:?]
    at ...
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field private java.lang.String java.lang.Throwable.detailMessage accessible: module java.base does not "opens java.lang" to unnamed module @2a742aa2
    at java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354) ~[?:?]
    at ...

Actuator should include the appropriate TypeAdapters to serialize Health objects with Gson out of the box by providing an appropriate GsonBuilderCustomizer for Health that deals with the exception property.

Alternatively, instead of just fixing this specifically for Health, an adapter for Throwable would be great in StandardGsonBuilderCustomizer, since that can probably be useful in other places as well (e.g. in @ExceptionHandler methods).

Side note: As a workaround, I also tried to set spring.gson.exclude-fields-without-expose-annotation=true, but while that avoided the exception, it resulted in an empty object being returned, since the Health object is not annotated for Gson.

Comment From: wilkinsona

Thanks for the report but Actuator requires Jackson and we do not support the use of Gson to serialize Actuator responses to json. There's some more information in https://github.com/spring-projects/spring-boot/issues/13766. In that issue, @philwebb wondered if we should fail hard when an attempt is made to use Gson:

I wonder if we should try to fail hard if GSON is used? We're getting a few bug reports because it appears to work even though we don't support it.

Perhaps we need to consider that again? It may be too late to make that change in 2.7.x and the benefit would be less in 3.0 as Actuator now has its own, isolated ObjectMapper.

Comment From: creckord

Okay, good to know - it's a bit surprising when an "innocent" supported config breaks support elsewhere, and the dependency on Jackson is not really clear from the outside, but glad that this is resolved in 3.x.

Any way that I can tell Actuator in 2.7.x to use Jackson in the meantime, while keeping Gson in my production endpoints? I'm using a separate port for management if that makes any difference.

Comment From: creckord

Hm, from the bug you referenced:

We've just fixed https://github.com/spring-projects/spring-boot/issues/12951, so as of Spring Boot 2.3, an application can choose Gson as a preferred JSON mapper and not risk Actuator endpoint responses being broken.

https://github.com/spring-projects/spring-boot/issues/12951 also makes the JSON mapper actuator-specific and independent of application configuration.

That's not what I am observing right now. All we are doing is this in application.yaml

spring:
  mvc:
    converters:
      preferred-json-mapper: gson

and in code:

@Configuration
public class WebMvcConfig {
    @Bean
    public GsonBuilderCustomizer gsonCustomizer() {
        return gsonBuilder -> {
           //custom type mappers
        };
    }
}

Jackson is present in the build, but Actuator seems to use the main GsonHttpMessageConverter with this config.

Comment From: wilkinsona

12951 is a little bit confusing, to me anyway, as the change was eventually reverted. @bclozel can you recall why the issue was left in the milestone after the revert?

Comment From: bclozel

@wilkinsona the revert happened after the M2 release, so we left the issue in place as M2 really contains that change.

Comment From: philwebb

We're going to look to see if we can add a DTO so that Jackson and GSON will both work. We're not going to attempt to support all actuator endpoints with GSON, but we can at least try to make health work again.

Comment From: mhalbritter

I found a way to revert the changes in Health.