Search before asking

  • [x] I searched in the issues and found nothing similar.

Describe the bug

Java Records fail to include @class type information when using DefaultTyping.NON_FINAL, causing Redis caching deserialization failures in Spring Boot applications.

Version Information

2.17.3

Reproduction

@Configuration
public class RedisConfig {
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                    .fromSerializer(new GenericJackson2JsonRedisSerializer(createRedisObjectMapper())));
        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(cacheConfiguration)
                .build();
    }

    private static ObjectMapper createRedisObjectMapper() {
        final ObjectMapper mapper = new ObjectMapper();
        mapper.activateDefaultTyping(
            mapper.getPolymorphicTypeValidator(),
            DefaultTyping.NON_FINAL,  // Records are excluded here
            JsonTypeInfo.As.PROPERTY
        );
        return mapper;
    }
}
public record UserRecord(Long id, String name) {}
@Service
public class UserService {
    @Cacheable("users")
    public UserRecord getUser(Long id) {
        return new UserRecord(id, "User-" + id);
    }
}
@Test
void testRecordCaching() {
    UserRecord user1 = userService.getUser(1L); // ✅ Works
    UserRecord user2 = userService.getUser(1L); // ❌ Fails
}

exception message: com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve subtype of [simple type, class java.lang.Object]: missing type id property '@class'

Expected behavior

No response

Additional context

Related to Previous Issue #3512:

This issue was previously reported in #3512 and closed with the following recommendations: - Use DefaultTyping.EVERYTHING instead of NON_FINAL for Records - Consider using regular classes instead of Records

However, these solutions have limitations:

DefaultTyping.EVERYTHING concerns: - Planned for deprecation and removal in Jackson 3.0 (#4160) - Includes unnecessary type metadata for all fields, raising security concerns - No clear migration path for Jackson 3.0

"Avoid Records" approach issues: - High risk of human error (developers forgetting which pattern to use)

Thank you for your consideration.

Comment From: JooHyukKim

First, it would help alot if you can provide a reproduction with only-Jackson? Something like this from our existing test suite would suffice.

Second, DefaultTyping.NON_FINAL does really mean non-final types, as per documentation...

Image

... so Java Record not being included in DefaultTyping.NON_FINAL configuration is a natural behavior. Also, there is another solution suggested -- check out the thread and you will find most answers @cndqjacndqja .

Comment From: cndqjacndqja

@JooHyukKim First of all, thank you for the quick response

I believe NON_FINAL excludes Records because they are final classes that don't support polymorphism, assuming that @class type information wouldn't be needed during deserialization. Reference documentation

The issue occurs in the following case:

public class JacksonTest {

    @Test
    public void testRecordDeserialization() throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        mapper.activateDefaultTyping(
            mapper.getPolymorphicTypeValidator(),
            DefaultTyping.NON_FINAL,
            JsonTypeInfo.As.PROPERTY
        );

        User user = new User(1L, "test-user");
        String json = mapper.writeValueAsString(user);

        // This fails: missing @class property
        Object result = mapper.readValue(json, Object.class);
    }
}

record User(Long id, String name) {

}

When running this test code, the following error occurs:

com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve subtype of [simple type, class java.lang.Object]: missing type id property '@class'
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 27]

This appears to be the same issue that occurs when GenericJackson2JsonRedisSerializer uses ObjectMapper. GenericJackson2JsonRedisSerializer deserializes using ObjectMapper as follows:

@Override 
public @Nullable Object deserialize(byte @Nullable[] source) throws SerializationException { 
    return deserialize(source, Object.class); 
}

GenericJackson2JsonRedisSerializer

Since it deserializes to Object.class, Records fail due to missing @class type information. Therefore, as shown in the original issue example, when implementing Redis caching using GenericJackson2JsonRedisSerializer, caching Records results in errors.

My question is: Should this be considered a problem with how GenericJackson2JsonRedisSerializer uses ObjectMapper? Was ObjectMapper originally designed with the expectation that classes without polymorphism (like Records or final classes) should NOT be deserialized to Object.class?

Thank you for your time and consideration.