Affects: Spring 5.3.21

This is somewhat related to #26321 - Spring using kotlin serialization over jackson (but not the same, as that's for WebMVC).

In Reactive Web, Kotlin classes that are tagged as @Serializable use Kotlin Serializers, not Jackson. This is a reasonable default, but changing the behaviour is very difficult, and has a few surprising side effects.

In Spring WebMVC, we could re-order the HttpMessageConverter<>, and put Jackson first:

// The approach we've used for WebMVC - there's no analogus support in WebFlux.
@Configuration
class WebConfig : WebMvcConfigurationSupport() {
  override fun configureMessageConverters(converters: MutableList<HttpMessageConverter<*>?>) {
    super.addDefaultHttpMessageConverters(converters)
    converters.sortBy { converter -> if (converter is KotlinSerializationJsonHttpMessageConverter) 1000 else 0 }
  }
}

The contract of WebFluxConfigurer doesn't allow modification of the list - because the BaseCodecConfigurer returns a new list each time:

    @Override
    public List<HttpMessageWriter<?>> getWriters() {
        this.defaultCodecs.applyDefaultConfig(this.customCodecs);

        List<HttpMessageWriter<?>> result = new ArrayList<>();
        result.addAll(this.customCodecs.getTypedWriters().keySet());
        result.addAll(this.defaultCodecs.getTypedWriters());
        result.addAll(this.customCodecs.getObjectWriters().keySet());
        result.addAll(this.defaultCodecs.getObjectWriters());
        result.addAll(this.defaultCodecs.getCatchAllWriters());
        return result;
    }

Therefore, adding any sort like in WebMVC has no effect.

Changing the Kotlin encoder to null (to try to disable), doesn't work, as BaseDefaultCodecs simply adds it back:

    @Override
    public void kotlinSerializationJsonEncoder(Encoder<?> encoder) {
        this.kotlinSerializationJsonEncoder = encoder;
        initObjectWriters(); // triggers a call to getBaseObjectWriters()
    }

       final List<HttpMessageWriter<?>> getBaseObjectWriters() {
        List<HttpMessageWriter<?>> writers = new ArrayList<>();
        if (kotlinSerializationJsonPresent) {
            addCodec(writers, new EncoderHttpMessageWriter<>(getKotlinSerializationJsonEncoder()));
        }
        ...snip...
        return writers;
    }

The workaround I've used is to put a decorator around the configurer to re-order every single time. However, this seems awkward.

@Configuration
class CustomerWebFluxConfigSupport : WebFluxConfigurationSupport() {
   override fun serverCodecConfigurer(): ServerCodecConfigurer {
      return ReOrderingServerCodecConfigurer(super.serverCodecConfigurer())
   }

   class ReOrderingServerCodecConfigurer(private val configurer: ServerCodecConfigurer) :
      ServerCodecConfigurer by configurer {

      override fun getWriters(): MutableList<HttpMessageWriter<*>> {
         val writers = configurer.writers
         val jacksonWriterIndex =
            configurer.writers.indexOfFirst { it is EncoderHttpMessageWriter && it.encoder is Jackson2JsonEncoder }
         val kotlinSerializationWriterIndex =
            configurer.writers.indexOfFirst { it is EncoderHttpMessageWriter && it.encoder is KotlinSerializationJsonEncoder }

         if (kotlinSerializationWriterIndex == -1 || jacksonWriterIndex == -1) {
            return writers
         }

         if (kotlinSerializationWriterIndex < jacksonWriterIndex) {
            Collections.swap(writers, jacksonWriterIndex, kotlinSerializationWriterIndex)
         }
         return writers
      }
   }
}

Expected / Desired Behaviour

It'd be nice if there was an easier way to configure this.

At the very least, where BaseDefaultCodecs overwrites the changed Kotlin serializer feels like a bug.

Comment From: rstoyanchev

Currently Kotlin serialization is based on classpath detection only. I'm just wondering, should it be on the classpath, and is there a way to exclude it? Or otherwise it would make sense to provide a way to disable it. @sdeleuze what do you think?

@martypitt, if you register a custom Encoder or Decoder (e.g. Jackson), it is ahead of default ones in the order, so that provides another option to influence the order.

Comment From: sdeleuze

@martypitt Could you please share more about your use case for using Jackson on classes annotated with @Serializable?

Comment From: martypitt

@martypitt Could you please share more about your use case for using Jackson on classes annotated with @Serializable?

Sure. We use JSON for responses out to web requests, as part of our "public" api. We use CBOR for serializing objects to put onto Kafka messages, or other downstream internal services.

Several classes are intended for serializaton in both scenarios. We also have a large number of custom Jackson serialization adaptors / converters, and didn't see a need to migrate away to the less mature Kotlin Serialization for JSON.

Comment From: sdeleuze

Could you please clarify where the Kotlinx serialization dependency come from? Declared directly in your project (for which use case) or via a third party dependency (which one)?

Comment From: martypitt

Sure.

In our spring boot app, we have the following:

<dependency>
   <groupId>org.jetbrains.kotlinx</groupId>
   <artifactId>kotlinx-serialization-cbor</artifactId>
</dependency>

We use this for serializing efficient messages between our own components.

Not sure if I've answered the question you're asking - please let me know if I can provide any other info.

Comment From: sdeleuze

Yes you did, but our classpath detection is based on kotlinx.serialization.json.Json which is expected to be a class specific to kotlinx-serialization-json dependency. So I am not sure why kotlinx-serialization-cbor triggers it, could you please check on your project?

Comment From: spring-projects-issues

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Comment From: spring-projects-issues

Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.

Comment From: volkert-fastned

In response to the following reply by @rstoyanchev :

Currently Kotlin serialization is based on classpath detection only. I'm just wondering, should it be on the classpath, and is there a way to exclude it? Or otherwise it would make sense to provide a way to disable it. @sdeleuze what do you think?

@martypitt, if you register a custom Encoder or Decoder (e.g. Jackson), it is ahead of default ones in the order, so that provides another option to influence the order.

@sdeleuze As you can see, I'm not the only one who's having problem with this default and unexpected classpath-dependent behavior. This is seems like the same problem I described in #32382 and #32384.

Could we maybe have a wider discussion about this problem? I would also very much appreciate some input here from your fellow developers. Thanks.

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

Comment From: rocketraman

I have the opposite problem -- Jackson is always used in favor of kotlinx-serialization. Man, Spring is... frustrating.

Comment From: hantsy

@rocketraman Check here, to use kotlinx serialization: * Add kotlinx-serialiazation-json to dependencies * Annotate your POJOs with @kotlinx.serialization.Serializable