question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Serialized result of java Class & Record is different with RedisTemplate

See original GitHub issue

Describe the bug Serialized result of java Class & Record is different with RedisTemplate.

Version information 2.13.3

To Reproduce

====== RedisConfiguration ======
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnClass(RedisOperations.class)
public class RedisConfiguration {

    @Bean
    public RedisTemplate<String, ?> jacksonRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        final RedisSerializer<String> stringRedisSerializer = new StringRedisSerializer();

        final RedisTemplate<String, ?> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        final GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = genericJackson2JsonRedisSerializer();

        template.setKeySerializer(stringRedisSerializer);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setHashKeySerializer(stringRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);

        return template;
    }

    @Bean
    public GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer() {
        final ObjectMapper mapper = new ObjectMapper()
            .registerModule(new ParameterNamesModule(JsonCreator.Mode.DEFAULT))
            .registerModule(new Jdk8Module())
            .registerModule(new JavaTimeModule())
            .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

        mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(),
                                     DefaultTyping.NON_FINAL,
                                     As.PROPERTY);

        return new GenericJackson2JsonRedisSerializer(mapper);
    }
}

====== RequestDto ======
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RequestDto {

    private Long id;
    private String name;
    private OffsetDateTime createdAt;
}

====== RequestRecord ======
public record RequestRecord(Long id,
                            String name,
                            OffsetDateTime createdAt) {
}


====== Test Code ======
@SpringBootTest
class RedisRecordTest {

    @Autowired
    private RedisTemplate<String, RequestRecord> redisRecordTemplate;
    @Autowired
    private RedisTemplate<String, RequestDto> redisDtoTemplate;
    @Autowired
    private RedisTemplate<String, String> redisStringTemplate;
    @Autowired
    private RedisTemplate<String, Object> redisObjectTemplate;
    @Autowired
    private ObjectMapper mapper;

    @DisplayName("record -> String -> set() -> get() -> String -> record -> O")
    @Test
    void test() throws JsonProcessingException {
        final RequestRecord record = new RequestRecord(1L, "doljae", OffsetDateTime.now());
        final String serialized = mapper.writeValueAsString(record);
        System.out.println(serialized);
        redisStringTemplate.opsForValue().set("key", serialized);

        final String fromRedis = redisStringTemplate.opsForValue().get("key");
        System.out.println(fromRedis);
        final RequestRecord deserialized = mapper.readValue(fromRedis, RequestRecord.class);
        System.out.println(deserialized);
    }

    @DisplayName("Class -> set() -> get() -> Class -> O")
    @Test
    void test2() {
        final RequestDto dto = new RequestDto(1L, "doljae", OffsetDateTime.now());
        redisDtoTemplate.opsForValue().set("key", dto);
        /*
         redis-cli
         get key
         -> "{\"@class\":\"com.example.redis.RequestDto\",\"id\":1,\"name\":\"doljae\",\"createdA\":\"2022-06-08T21:39:01.166705+09:00\"}"
         */

        final RequestDto fromRedis = redisDtoTemplate.opsForValue().get("key");
        System.out.println(fromRedis);
    }

    @DisplayName("record -> set() -> get() -> record -> X")
    @Test
    void test3() {
        final RequestRecord record = new RequestRecord(1L, "seokjae", OffsetDateTime.now());
        redisRecordTemplate.opsForValue().set("key", record);
        /*
         redis-cli
         get key
         -> "{\"id\":1,\"name\":\"doljae\",\"createdAt\":\"2022-06-08T21:40:36.057138+09:00\"}"
         */

        // Caused by: com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve subtype of [simple type, class java.lang.Object]: missing type id property '@class'
        // at [Source: (byte[])"{"id":1,"name":"doljae","createdAt":"2022-06-08T21:40:36.057138+09:00"}"; line: 1, column: 72]
        final RequestRecord deserialized = redisRecordTemplate.opsForValue().get("key");
    }
}

Expected behavior Both class and record objects should be stored and deserialized normally in RedisTemplate.

Additional context

  mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(),
                                     DefaultTyping.EVERYTHING,
                                     As.PROPERTY);
  • but this way exposes every meta data of object’s fields…

Issue Analytics

  • State:open
  • Created a year ago
  • Comments:10 (4 by maintainers)

github_iconTop GitHub Comments

2reactions
cowtowncodercommented, Jun 8, 2022

@doljae getting the ObjectMapper configuration is something that Spring Data Redis users or maintainers can help with.

I think your problem can be divided in two parts:

  1. How to make Jackson (without considering Redis template) to work the way you want – if possible, keep in mind that Polymorphic Deserialization by definition may expose internal aspects that differ between differ object types
  2. How to configure framework around to use settings.

But one other thing I would STRONGLY recommend: instead of attempting to serialize any given “Root value” directly, you really should instead use a wrapper if possible: something like:

public class Wrapper {
   @JsonTypeInfo(....)
   public Object wrapped; // or whatever name

   // and/or getters, setters for accessing wrapped value 
}

This will avoid many pitfalls, including problem of Java Type Erasure for the root value. It may also remove the need to active default typing completely (although this depends a little bit on kind of data you serialize).

In your case, for example, one problem is that Record types are final and non-polymorphic, and as such inclusion of type id may not occur as you’d expect, if such value is serialized directly.

2reactions
yawkatcommented, Jun 8, 2022

i can reproduce your issue with the new test case, and yes it is because of DefaultTyping.NON_FINAL. Changing it to DefaultTyping.EVERYTHING resolves this. However it is somewhat problematic because it also adds default typing to unrelated places like the Long field.

A better alternative is to use mapper.writerFor(Object.class).writeValueAsBytes(record). This adds the type information even in the NON_FINAL case. However I can’t say how to make spring use this approach.

Read more comments on GitHub >

github_iconTop Results From Across the Web

How to Serialize to Java Object in Spring Data Redis
This is my redis Template that i have defined in my @configuration class, it's taking a jedis Connection factory, i have set my...
Read more >
Spring Data Redis
RedisTemplate uses a Java-based serializer for most of its operations. This means that any object written or read by the template is serialized...
Read more >
Simpler object and data serialization using Java records
Serialization with records ... With Java Serialization, a record class is made serializable just like a normal class, simply by implementing java.
Read more >
Introduction to Spring Data Redis - Baeldung
There is a number of Redis client implementations available for Java. In this tutorial, we'll use Jedis — a simple and powerful Redis...
Read more >
How to use Redis-Template in Java Spring Boot? - Medium
Spring encapsulates a more powerful template, redisTemplate, ... The same operation as leftPush , the only difference is that the value of ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found