springspring-bootkotlinredis

How to cache serializable data class to Redis using @Cacheable in spring boot?


I am trying to cache a Kotlin data class(serializable) to Redis, but I am getting this error-

org.springframework.data.redis.serializer.SerializationException: Cannot deserialize
at org.springframework.data.redis.serializer.JdkSerializationRedisSerializer.deserialize(JdkSerializationRedisSerializer.java:113) ~[spring-data-redis-3.5.1.jar:3.5.1]

I am using the Kotlin serialization plugin (version: 2.2.0) and dependency (version: 1.9.0) in my project. Spring 3.5.3

Service -

 @Cacheable("Schedule")
 suspend fun getTrainSchedule(trainNumber: String): TrainDetails? {
    val data = trainDetailDao.getSchedule(trainNumber)
     return data
}

Data class -

import kotlinx.serialization.Serializable

@Serializable
data class TrainDetails(
    val no: String,
    val na: String,
    val typ: Int,
    val dly: Int,
    val zn: Int, 
    val rake: String,
    val classes: Int,
    val upd: String,
    )

Solution

  • Spring’s default Redis caching uses JDK serialization, which doesn’t work with Kotlin data classes unless they implement Serializable. To store Kotlin @Serializable classes as JSON (String) in Redis, you can use a custom RedisSerializer built on Kotlinx Serialization.

    Define the Kotlinx Redis Serializer

    import kotlinx.serialization.KSerializer
    import kotlinx.serialization.json.Json
    import kotlinx.serialization.encodeToString
    import kotlinx.serialization.decodeFromString
    import org.springframework.data.redis.serializer.RedisSerializer
    import org.springframework.data.redis.serializer.SerializationException
    import java.nio.charset.StandardCharsets
    
    class KotlinxRedisSerializer<T : Any>(
        private val serializer: KSerializer<T>,
        private val json: Json = Json
    ) : RedisSerializer<T> {
    
        override fun serialize(t: T?): ByteArray? {
            return try {
                if (t == null) return null
                json.encodeToString(serializer, t).toByteArray(StandardCharsets.UTF_8)
            } catch (e: Exception) {
                throw SerializationException("Could not serialize: $t", e)
            }
        }
    
        override fun deserialize(bytes: ByteArray?): T? {
            return try {
                if (bytes == null || bytes.isEmpty()) return null
                val str = String(bytes, StandardCharsets.UTF_8)
                json.decodeFromString(serializer, str)
            } catch (e: Exception) {
                throw SerializationException("Could not deserialize", e)
            }
        }
    }
    

    Add a reified helper

    import kotlinx.serialization.serializer
    
    inline fun <reified T : Any> kotlinxRedisSerializer(
        json: Json = Json
    ): RedisSerializer<T> {
        return KotlinxRedisSerializer(serializer = json.serializersModule.serializer(), json = json)
    }
    

    Configure RedisCacheManager to Use It

    @Configuration
    class RedisConfig {
    
        @Bean
        fun redisCacheManager(factory: RedisConnectionFactory): RedisCacheManager {
            val serializer = kotlinxRedisSerializer<TrainTicket>()
    
            val config = RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(
                    RedisSerializationContext.SerializationPair.fromSerializer(serializer)
                )
    
            return RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build()
        }
    }
    

    I also added a full working demo to the "redis-springboot-resources" repository:

    https://github.com/redis-developer/redis-springboot-resources/tree/main/caching/caching-with-kotlinx-serialization