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