Actuator APIs (Logger for example) are using
Map deserializer when JSON is received and ObjectMapper when it is send. Which causes mismatched messages.
In my case I want ObectMapper to be configured with snake case and keep Maps unchanged. If I do this I have to use lower camel notation on my POST/PUT methods and snake case in GETs.
Comment From: wilkinsona
Thanks for the report, but I'm not sure that I have understood the problem. The Actuator uses an ObjectMapper
instance for JSON serialisation and deserialisation. Are you observing a difference when mapping a POJO vs a Map, perhaps? To remove any doubt, can you please provide a minimal example that reproduces the problem? You can share it with us by pushing it to a separate repository on GitHub or by zipping it up and attaching it to this issue.
Comment From: mkarasik
Sorry for not clear explanation. I am not too familiar with Spring terminology. I have the following ObjectMapper configuration in my app
WebMvcConfigurer
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
for (var converter : converters) {
if (converter instanceof MappingJackson2HttpMessageConverter) {
var objectMapper = ((MappingJackson2HttpMessageConverter) converter).getObjectMapper();
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
}
}
}
It is probably not perfect code but that is what I have.
After doing this I found out that actuator Logger endpoint returns 'snake case' JSONs in GET requests but still require lower case camel notation in POST.
I spent some time in debugger and found that somewhere deep inside Spring code it is using Map deserializer to extract request parameters.
It is coming from
ServletWebOperationAdapter
@Override
public Object handle(HttpServletRequest request,
@RequestBody(required = false) Map<String, String> body) {
you can see that body is already presented as Map here, so further processing in ReflectiveOperationInvoker.resolveArguments can't resolve arguments if they are sent in snake case.
Comment From: wilkinsona
This is Jackson's standard behaviour. It treats Map and POJO deserialisation differently and you have only configured its POJO deserialisation behaviour. If you want to customise the map key deserialisation, you'll have to configure Jackson with a custom key deserializer to convert the received snake_case into the required camelCase. You can do so with a SimpleModule
that's exposed as a bean:
@Bean
Module customKeyDeserializer() {
SimpleModule module = new SimpleModule();
module.addKeyDeserializer(String.class, new KeyDeserializer() {
@Override
public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException {
StringBuilder result = new StringBuilder();
char previous = 0;
for (char c : key.toCharArray()) {
if (c != '_') {
result.append(previous == '_' ? Character.toUpperCase(c) : c);
}
previous = c;
}
return result.toString();
}
});
return module;
}
Comment From: wilkinsona
See also https://github.com/spring-projects/spring-boot/issues/22950 for a somewhat similar problem with the serialisation of error responses and #20291 for providing an Actuator-specific ObjectMapper
.
I'm going to close this one. Generally speaking we don't recommend try to customise the form of the JSON that the actuator accepts and produces. That's not easy right now as the ObjectMapper
is shared with the application. #20291 will address that. In the meantime, the approach shown above should be used if you want to change the format of keys that the actuator consumes.
Comment From: mkarasik
Yes, I configured it differently and I want them to be different.
My complain is that actuator APIs are processed asymmetric: input is de-serialized through Map and output through POJO. I can try to configure custom map deserializer for Actuator endpoints only (not sure if this is possible at all) but I still think this is actuator design problem, not my configuration issue.