Skip to content

Spring Reactive Web uses kotlin serialization over jackson for its own classes, and it's very hard to change #28856

@martypitt

Description

@martypitt

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.

Metadata

Metadata

Assignees

Labels

in: webIssues in web modules (web, webmvc, webflux, websocket)theme: kotlinAn issue related to Kotlin support

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions