I'm building a Spring WebFlux API with Kotlin and using Kotlinx Serialization for JSON serialization.
build.gradle.kts
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.spring)
alias(libs.plugins.spring.boot)
alias(libs.plugins.spring.dependency.management)
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-webflux:3.5.3")
{
exclude(group = "com.fasterxml.jackson.core", module = "jackson-databind")
exclude(group = "com.fasterxml.jackson.module", module = "jackson-module-kotlin")
}
implementation(libs.kotlinX.serialization)
}
libs.version.toml-
[versions]
spring = "3.5.3"
kotlin = "2.2.0"
serialization = "1.9.0"
kotlinX-serialization = {module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
spring-boot = { id = "org.springframework.boot", version.ref = "spring" }
spring-dependency-management = { id = "io.spring.dependency-management", version = "1.1.7" }
My controller method looks like this:
@GetMapping("/trainSch", produces = [MediaType.APPLICATION_JSON_VALUE])
suspend fun getTrainSch(
@RequestParam("trainNo") trainNumber: String
): ResponseEntity<TrainDetails> {
return ResponseEntity.ok(
TrainDetails(
no = trainNumber,
na = "Superfast Express",
typ = 1,
zn = 5,
dly = 10,
rake = "LHB",
classes = 3,
upd = "10:45 AM"
)
)
}
TrainDetails is a Kotlin data class:
@Serializable
data class TrainDetails(
val no: String,
val na: String,
val typ: Int,
val zn: Int,
val dly: Int,
val rake: String,
val classes: Int,
val upd: String
)
I have registered the Kotlinx Serialization encoder/decoder with a WebFluxConfigurer bean:
@Bean
fun webFluxConfigurer(
encoder: KotlinSerializationJsonEncoder,
decoder: KotlinSerializationJsonDecoder
): WebFluxConfigurer = object : WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
configurer.defaultCodecs().kotlinSerializationJsonEncoder(encoder)
configurer.defaultCodecs().kotlinSerializationJsonDecoder(decoder)
}
}
Problem: When I make a GET request to /trainSch?trainNo=12345, the debug logs show:
TRACE ... CharSequenceEncoder : Writing "{\"no\":\"12345\",\"na\":\"Superfast Express\",...}"
Instead of:
TRACE ... KotlinSerializationJsonEncoder : Writing {...}
It seems Spring is using CharSequenceEncoder (which encodes Strings) instead of my Kotlinx JSON encoder.
Response in Postman-
Content-Type:application/json
content-encoding:gzip
content-length:123
{
"no": "12232",
"na": "Superfast Express",
"typ": 1,
"dly": 10,
"zn": 5,
"rake": "LHB",
"classes": 3,
"upd": "10:45 AM"
}
Actually, there is no issue. CharSequenceEncoder
by itself cannot serialize anything to JSON. It can only write a CharSequence
to a DataBuffer
.
What happens is that Spring WebFlux uses KotlinSerializationJsonEncoder
to serialize the class to a String
, and then delegates to CharSequenceEncoder
to encode that String
into a DataBuffer
. You can see this in the source code of KotlinSerializationStringEncoder
, which is the superclass of KotlinSerializationJsonEncoder
:
@Override
public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory,
ResolvableType valueType, @Nullable MimeType mimeType,
@Nullable Map<String, Object> hints) {
KSerializer<Object> serializer = serializer(valueType);
if (serializer == null) {
throw new EncodingException("Could not find KSerializer for " + valueType);
}
String string = format().encodeToString(serializer, value);
return this.charSequenceEncoder.encodeValue(string, bufferFactory, valueType, mimeType, null);
}
There’s even a comment about this:
// CharSequence encoding needed for now, see https://github.com/Kotlin/kotlinx.serialization/issues/204 for more details
private final CharSequenceEncoder charSequenceEncoder = CharSequenceEncoder.allMimeTypes();
By the way, in Spring Boot 3.5.x, you don’t need to manually exclude Jackson to make Kotlin Serialization work. You just need to have kotlinx.serialization.json.Json
on the classpath (which you already do), and Spring will register KotlinSerializationJsonEncoder
with a higher priority than Jackson. That means this part in your build.gradle.kts
is redundant:
{
exclude(group = "com.fasterxml.jackson.core", module = "jackson-databind")
exclude(group = "com.fasterxml.jackson.module", module = "jackson-module-kotlin")
}
Note that this behavior will change in Spring Boot 4.0.x.