Skip to content

RedisCache with GenericJacksonJsonRedisSerializer breaks when using enums #3306

Open

Description

@chanho0912

When using GenericJacksonJsonRedisSerializer, enum values are always serialized without type metadata, even when DefaultTyping.NON_FINAL_AND_ENUMS is explicitly configured. This causes ClassCastException during deserialization.

Example

public enum Status {
    ACTIVE, INACTIVE
}

@Cacheable(cacheNames = "status", cacheManager = "redisCacheManager")
public Status getStatus(String id) {
    return Status.ACTIVE;
}

Serialized value in Redis:

"ACTIVE"

Exception on deserialization:

java.lang.ClassCastException: class java.lang.String cannot be cast to class Status

Configuration Attempt

GenericJacksonJsonRedisSerializer.create(b -> {
    b.enableDefaultTyping(ptv);
    b.customize(mb -> 
        mb.activateDefaultTypingAsProperty(
            ptv,
            DefaultTyping.NON_FINAL_AND_ENUMS,
            "@class"
        )
    );
});

Using customize() to override with Jackson's default DefaultTypeResolverBuilder does enable enum type metadata, but it loses Spring's Kotlin-specific handling for final classes:

// This Kotlin support in Spring's TypeResolverBuilder is lost:
if (javaType.isFinal() && !KotlinDetector.isKotlinType(javaType.getRawClass())
        && javaType.getRawClass().getPackageName().startsWith("java")) {
    return false;
}

This forces users to choose between enum support and Kotlin data class support.

Root Cause

Spring's TypeResolverBuilder unconditionally excludes enum types regardless of the DefaultTyping setting:

https://github.com/spring-projects/spring-data-redis/blob/main/src/main/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializer.java#L631-L633

if (javaType.isEnumType() || ClassUtils.isPrimitiveOrWrapper(javaType.getRawClass())) {
    return false;  // Always excluded
}

Suggested Fix

Store the DefaultTyping parameter and use it in useForType():

private static class TypeResolverBuilder extends DefaultTypeResolverBuilder {

    private final DefaultTyping defaultTyping;

    public TypeResolverBuilder(PolymorphicTypeValidator subtypeValidator, DefaultTyping defaultTyping, 
            JsonTypeInfo.As includeAs, JsonTypeInfo.Id idType, @Nullable String propertyName) {
        super(subtypeValidator, defaultTyping, includeAs, idType, propertyName);
        this.defaultTyping = defaultTyping;
    }

    @Override
    public boolean useForType(JavaType javaType) {
        if (javaType.isJavaLangObject()) {
            return true;
        }

        javaType = resolveArrayOrWrapper(javaType);

        // Respect DefaultTyping.NON_FINAL_AND_ENUMS for enum types
        if (javaType.isEnumType()) {
            return defaultTyping == DefaultTyping.NON_FINAL_AND_ENUMS;
        }

        if (ClassUtils.isPrimitiveOrWrapper(javaType.getRawClass())) {
            return false;
        }

        if (javaType.isFinal() && !KotlinDetector.isKotlinType(javaType.getRawClass())
                && javaType.getRawClass().getPackageName().startsWith("java")) {
            return false;
        }

        return !TreeNode.class.isAssignableFrom(javaType.getRawClass());
    }

    // ... resolveArrayOrWrapper() unchanged
}

This preserves backward compatibility (enums excluded by default with NON_FINAL) while allowing enum type metadata when explicitly requested via NON_FINAL_AND_ENUMS.

This preserves backward compatibility while allowing enum type metadata when explicitly requested.

Current Workarounds

  1. Wrap enums in a generic container class
  2. Copy and modify Spring's TypeResolverBuilder (not maintainable)

Environment

  • Spring Data Redis: 4.0.x
  • Jackson: 3.x

Activity

  1. self-assigned this
    on Feb 9, 2026
  2. chanho0912 commented on Feb 10, 2026

    @chanho0912
    Author

    @mp911de
    Hi, I see this has been assigned already, but I'd be happy to submit a PR if that would be helpful. Please let me know.

  3. mp911de commented on Feb 10, 2026

    @mp911de
    Member

    Thank you for your issue report. We appreciate community contributions, but we do need them to meet a reasonable baseline for consideration.

    Tip

    This means:

    • Expressing your intent with a concise and reasonable description
    • Providing what situation you've encountered, describing your setup, ideally with a reproducer provided as GitHub repository, and what you would expect

    When issues require significant effort from our side to initially understand your request, we have to deprioritize or even reject them in favor of tickets that are ready for discussion as we have to be mindful of our limited time, and we need to focus it where it has the most impact. If you would like us to spend some time working on this issue, please spend some time making this a substantial and quality ticket description.

    When describing your concern, please focus on the aspects that matter. There's no need to duplicate parts of our code in the description or use excessive summarization or bullet lists. Instead, use rather links if you want to point out subtle details that matter in the specific context. Avoid conflicting requests such as asking for guidance and then answering questions yourself, as such descriptions neglect raising a ticket in the first place.

    Important

    Help us understand:

    • Your reasoning for the approach you've chosen
    • Trade-offs you considered
    • Any areas where you're uncertain and would value guidance

    Concise, thoughtful explanations of your decision-making process are far more valuable than detailed restatements of the code.

    Hi, I see this has been assigned already, but I'd be happy to submit a PR if that would be helpful. Please let me know.

    I've explored your suggestion and it seems reasonable. I've created a pull request #3309 myself as it makes sense to mirror settings between Jackson 2 and Jackson 3 variants and our configuration code has quite some nuances that even AI-assisted code generation gets wrong and that requires much more time on our side instead of us simply doing the changes.

    Just adding DefaultTyping isn't likely going to achieve what you're looking for because accepting DefaultTyping requires us to adhere to the NON_FINAL part of NON_FINAL_AND_ENUMS, too. It would be a strange behavior to accept NON_FINAL_AND_ENUMS as typing configuration resulting with type hints being written for final types (such as records).

    I think this issue requires much more thought and an attempt would be a type hint registry that can be built from Predicate<JavaType> for more precise control over types that should be equipped with type hints.

  4. changed the title [-]GenericJacksonJsonRedisSerializer ignores DefaultTyping.NON_FINAL_AND_ENUMS for enum types[/-] [+]`RedisCache` with `GenericJacksonJsonRedisSerializer` breaks when using enums[/+] on Feb 10, 2026
  5. chanho0912 commented on Feb 11, 2026

    @chanho0912
    Author

    @mp911de
    Thank you for the quick fix and the feedback on the issue description.

  6. mp911de commented on Feb 11, 2026

    @mp911de
    Member

    Thank you for the quick fix

    This is not a fix. This is a draft that shows shortcomings of using DefaultTyping.

  7. added 3 commits that reference this issue on Mar 13, 2026
    3865339
    63ab6cb
    e506e63
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions