spring-projects / spring-projects/spring-data-redis

GenericJacksonJsonRedisSerializer default typing fails for DTO properties declared as Kotlin value classes

Open
#3,418 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

status: waiting-for-triage
Dominant language
Java
Stars
1.9k
Forks
1.3k
PR merge metrics
No merged PRs in 30d

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with GenericJacksonJsonRedisSerializer.TypeResolverBuilder.useForType() and compare its handling of Kotlin inline classes with the reproduction test in the issue. Add a regression test for a Kotlin value-class DTO property, then verify serialization keeps the shown JSON format and deserialization returns the original SampleResult.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, kotlin, redis, spring
Domain
backend, database
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.