Hello,
I included a library in a Kotlin Spring project that happens to include kotlinx.serialization
as a transitive dependency.
However, we are not yet ready to switch the entire Spring application from Jackson to kotlinx.serialization
, since that would result in different behavior, and break quite a few things. As a matter of fact, integration tests started to fail because of this exact reason.
Apparently, the preference for kotlinx.serialization
over Jackson (if found in the classpath) is hard-coded throughout multiple locations in the Spring source code, through use of a ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader)
check. Examples: AllEncompassingFormHttpMessageConverter, RestTemplate, DefaultRestClientBuilder, etc.
Why is Spring so opinionated on the preferential order when multiple serde frameworks are detected in the classpath and why is this hard-coded in such a way, not to mention in different parts (one RestTemplate, one for Feign, one for MVC, etc)?
Why isn't there a central way to select the preferred serde framework in application.yml
, for instance? I believe there used to be such settings in older versions of Spring Boot (spring.http.converters.preferred-json-mapper
, spring.mvc.converters.preferred-json-mapper
and such), but those didn't do anything for me.
I found several workarounds through @Configuration
beans that would have to filter the list of MessageConverters and such, but those don't seem to work for me and are also quite ugly workarounds, even if they would work.
The existence of certain libraries and dependencies (transitive or otherwise) in the classpath should not change application behavior like that, and even if that's somehow inevitable, it should be easy to override this in a convenient and single application-wide way.
Am I missing something?
Any help on forcing the Spring framework to prefer Jackson over kotlinx.serialization
application-wide would be helpful.
I'm not ruling out migrating to kotlinx.serialization
entirely at some point, but I prefer to first restore the existing behavior before attempting that.
Thanks for any help you can provide with this. 🙏🏽
Comment From: volkert-fastned
Hmmm... It appears that spring.http.converters.preferred-json-mapper
has been deprecated in favor of spring.mvc.converters.preferred-json-mapper
, implying that the latter is the general Spring configuration setting for the preferred JSON serde framework.
Perhaps a part of my problem is that because MockMvc
does not honor this setting in tests? Ah, maybe because spring.mvc.converters.preferred-json-mapper
is specific to Spring Boot?
By the way, it's also interesting how you can't explicitly select kotlinx.serialization
through spring.mvc.converters.preferred-json-mapper
, at least according to spring-configuration-metadata.json
. The only supported values appear to be gson
, jackson
and jsonb
.
Comment From: sdeleuze
For configuring kotlinx.serialization
via spring.mvc.converters.preferred-json-mapper
, if that's not supported, you may want to create a related PR or issue on Spring Boot side.
For the rest, it feels like this is a question that would be better suited to Stack Overflow. We prefer to use the issue tracker only for bugs and enhancements. Feel free to update this issue with a link to the re-posted question (so that other people can find it) or add some more details if you feel this is a genuine bug.
Comment From: volkert-fastned
@sdeleuze Thank you, but I already checked StackOverflow. Closest I came to an answer was this: https://stackoverflow.com/questions/64050458/force-spingboot-to-use-gson-over-jackson
And even there, many responders didn't manage to get it to work.
If there is no proper way to configure this in MockMvc to just folllow the Spring Boot configuration, it may very well be a bug.
At any rate, it really shouldn't be so hard to configure something as fundamental as this.
Just excluding the kotlinx.serialization
package from the component scanner isn't going to help either, because of the hard-coded ClassUtils.isPresent()
lookups, which just look directly in the classpath. No flags to override this or anything.
This is one of the following:
- A bug, since all the suggested workarounds with
@Configuration
and@Bean
configs and such aren't working - A feature request
- A lack of documentation
Wouldn't any one of these possibilities justify an open ticket here?
Comment From: volkert-fastned
Maybe I should have rephrased the issue: "serialization framework detection shouldn't be hard-coded as a classpath lookup"?
Comment From: volkert-fastned
@sdeleuze Thanks for your feedback. It's true that my opening post in this issue made it look like a technical question as opposed to a bug or feature request. So I've created a separate ticket, which goes into the (IMO problematic) Spring code specifically, and proposes an improvement.
Thanks for your feedback.
Comment From: sdeleuze
It is not hardcoded, see WebMvcConfigurer#configureMessageConverters
or this RestTemplate
constructor.
Comment From: volkert-fastned
@sdeleuze I'm sorry, but how can you say that this is not hard-coded?
static {
kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader);
}
This kind of stuff leads to to breakage of existing behavior, whenever something new gets detected on the classpath that happens to be listed there.
I've been spending hours trying to restore the original behavior that I had before I added the non-Spring library. Neither your links above nor anything I've found on StackOverflow so far has proved fruitful in getting MockMvc to play ball with this, and at this point I'm also concerned that I'm going to run into possible runtime issues in addition to that too, particularly ones that might not immediately manifest themselves.
It should really be possible and practical for developers to use both Spring and non-Spring stuff interchangeably without it suddenly breaking like this, and requiring me to figure out new configuration work that I didn't need before, and potentially introducing risky bugs.
I'm sorry that I'm replying and mentioning you again, but this is very frustrating, and had these presence checks in the Spring sources been somehow injected or overridable, that would have saved me so much time and effort. I may not be a Spring maintainer, but I'm sure there's a more elegant way than this to handle such auto-detection in the code. I know you've been working hard on this project for many years, but there's nothing wrong with some constructive criticism.
If you want, I can try to open a PR, to attempt to improve this. Would you be open to that?
Comment From: volkert-fastned
For anyone stumbling upon this thread with the same (or a similar) problem, I figured out the following workaround, which worked at least in my case:
import org.springframework.beans.factory.ObjectProvider
import org.springframework.boot.autoconfigure.http.HttpMessageConverters
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.converter.AbstractKotlinSerializationHttpMessageConverter
import org.springframework.http.converter.HttpMessageConverter
@Configuration
class MessageConvertersConfig {
/**
* Bean provider that filters out standard [HttpMessageConverter]s that we don't want Spring to use, even when the
* serde frameworks for them are detected by Spring in the classpath.
*
* Should override [org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration.messageConverters]
*/
@Bean
fun filteredMessageConverters(converters: ObjectProvider<HttpMessageConverter<*>>): HttpMessageConverters {
/**
* All standard [HttpMessageConverter]s, depending in part on serde frameworks detected by Spring in classpath.
*/
val standardConverters = HttpMessageConverters().converters
return HttpMessageConverters(
false,
standardConverters.filter { converter ->
converter !is AbstractKotlinSerializationHttpMessageConverter<*>
},
).also {
// This would fail if standardConverters were used directly without filtering above.
check(
!it.converters.any { converter ->
converter is AbstractKotlinSerializationHttpMessageConverter<*>
},
)
}
}
}
In my case it was related to the standard HttpMessageConverters
(including the auto-deteted ones like KotlinSerializationJsonHttpMessageConverter
) leaking into the Spring Cloud OpenFeign configuration.
the cause of the problem seems to lie in Spring Boot: HttpMessageConvertersAutoConfiguration.PREFERRED_MAPPER_PROPERTY
(application property spring.mvc.converters.preferred-json-mapper
) is effectively ignored when AllEncompassingFormHttpMessageConverter
detects the kotlinx.serialization
library in the classpath.
I'll open a ticket in the Spring Boot project for this.
Comment From: volkert-fastned
For anybody stumbling upon this thread through a search engine while looking for a solution to this problem in Spring Boot, see these answers for a workaround:
- https://github.com/spring-projects/spring-boot/issues/39853#issuecomment-1984360351
- https://github.com/spring-projects/spring-boot/issues/1482#issuecomment-61862787