Skip to content

GenericJacksonJsonRedisSerializer ignores DefaultTyping.NON_FINAL_AND_ENUMS for enum types #3306

@chanho0912

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions