Repository navigation
Description
Description
GenericJacksonJsonRedisSerializer fails to deserialize a DTO containing a Kotlin value-class property when the Jackson Kotlin module and Spring Data Redis default typing are enabled.
Serialization succeeds, but deserialization fails because Spring Data Redis's custom TypeResolverBuilder expects a polymorphic type ID for the declared value class, while the Jackson Kotlin module serializes it as its underlying scalar value.
The issue can be reproduced without running a Redis server.
Versions
- Spring Data Redis: 4.1.0
- Spring Framework: 7.0.8
- Jackson Databind: 3.1.4
- Jackson Kotlin Module: 3.1.4
- Kotlin: 2.3.21
- Java: 25.0.2
- Spring Boot: 4.1.0, used for dependency management
The same resolver logic is also present on the current main branch.
Reproduction
package example
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer
import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator
import tools.jackson.module.kotlin.kotlinModule
@JvmInline
value class SampleId(val value: Long)
data class SampleResult(
val sampleId: SampleId,
)
class GenericJacksonJsonRedisSerializerValueClassTest {
@Test
fun `round trips a Kotlin value class property`() {
val validator = BasicPolymorphicTypeValidator.builder()
.allowIfSubType("example.")
.build()
val serializer = GenericJacksonJsonRedisSerializer.builder()
.enableDefaultTyping(validator)
.customize { it.addModule(kotlinModule()) }
.build()
val original = SampleResult(SampleId(123L))
val bytes = serializer.serialize(original)
assertEquals(
"""{"@class":"example.SampleResult","sampleId":123}""",
bytes.toString(Charsets.UTF_8),
)
assertEquals(original, serializer.deserialize(bytes))
}
}The second assertion fails.
Actual behavior
Serialization produces the following JSON:
{
"@class": "example.SampleResult",
"sampleId": 123
}Deserialization fails with:
org.springframework.data.redis.serializer.SerializationException:
Could not read JSON:
Could not resolve subtype of [simple type, class example.sampleId]:
missing type id property '@class' (for POJO property 'sampleId')
The direct cause is a tools.jackson.databind.exc.InvalidTypeIdException.
Expected behavior
serializer.deserialize(bytes) should return:
SampleResult(SampleId(123L))A DTO property whose declared type is a Kotlin value class should round-trip when the Jackson Kotlin module is registered.
Analysis
GenericJacksonJsonRedisSerializer.TypeResolverBuilder.useForType() currently contains the following logic:
if (javaType.isFinal() && !KotlinDetector.isKotlinType(javaType.getRawClass())
&& javaType.getRawClass().getPackageName().startsWith("java")) {
return false;
}This causes useForType() to return true for all non-primitive Kotlin types, including @JvmInline value classes.
The Jackson Kotlin module unboxes SampleId to its underlying long value during serialization. The scalar serializer does not emit polymorphic type information, so the sampleId property does not contain an @class field.
During deserialization, the Spring Data Redis type resolver applies a type deserializer to the declared SampleId property and requires the missing type ID.
This issue is specific to GenericJacksonJsonRedisSerializer with default typing. It is not a general limitation of Spring Data's Kotlin value-class support. A typed JacksonJsonRedisSerializer<SampleResult> with a Kotlin mapper and without default typing works correctly.
Suggested fix
Exclude Kotlin inline classes from default typing after resolving array and reference types:
javaType = resolveArrayOrWrapper(javaType);
if (KotlinDetector.isInlineClass(javaType.getRawClass())) {
return false;
}
if (javaType.isEnumType() || ClassUtils.isPrimitiveOrWrapper(javaType.getRawClass())) {
return false;
}KotlinDetector.isInlineClass(Class<?>) has been available since Spring Framework 6.1.5.
This change retains type information for ordinary final Kotlin DTOs while allowing the Jackson Kotlin module to reconstruct value-class properties from their declared property types.
It also keeps the serialized JSON format unchanged.
Scope
This report concerns value classes used as concrete DTO properties.
A value class used directly as the root value of an untyped generic serializer remains a separate limitation because scalar JSON does not retain the root value-class name.
The Jackson 2 GenericJackson2JsonRedisSerializer contains analogous resolver logic and may require the same exclusion.
I would be happy to submit a pull request with a regression test if this approach is acceptable.
Activity
exijn commentedon Aug 25, 2026
Hi, I'd like to investigate this issue.
I'll first create a minimal regression test using a Kotlin DTO with a value-class property,
GenericJacksonJsonRedisSerializer, the Jackson Kotlin module, and default typing. I'll compare it with an equivalent regular Kotlin property to identify whether the type metadata is lost during serialization or deserialization.If the issue is reproducible on the current main branch, I'll trace the interaction between Spring Data Redis's type resolver and Jackson's Kotlin value-class handling, then propose the smallest compatibility fix with regression coverage while preserving existing behavior for Java types and regular Kotlin classes.
Since this issue is still waiting for triage, would this approach and a contribution be welcome?