diff --git a/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java index 16a298b06..d6254443f 100644 --- a/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java +++ b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java @@ -16,8 +16,11 @@ package org.springframework.data.redis.serializer; import java.io.IOException; +import java.util.Collections; +import java.util.function.Supplier; import org.springframework.cache.support.NullValue; +import org.springframework.data.util.Lazy; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -25,14 +28,19 @@ import org.springframework.util.StringUtils; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo.As; import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; +import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; import com.fasterxml.jackson.databind.jsontype.TypeSerializer; import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.TextNode; import com.fasterxml.jackson.databind.ser.SerializerFactory; import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.fasterxml.jackson.databind.type.TypeFactory; /** * Generic Jackson 2-based {@link RedisSerializer} that maps {@link Object objects} to JSON using dynamic typing. @@ -47,12 +55,37 @@ import com.fasterxml.jackson.databind.ser.std.StdSerializer; */ public class GenericJackson2JsonRedisSerializer implements RedisSerializer { - private final ObjectMapper mapper; + private ObjectMapper mapper; private final JacksonObjectReader reader; private final JacksonObjectWriter writer; + private boolean internalReader = false; + + private final TypeResolver typeResolver; + + private Lazy defaultTypingEnabled = Lazy + .of(() -> mapper.getSerializationConfig().getDefaultTyper(null) != null); + + private Lazy typeHintPropertyName; + + { + typeHintPropertyName = Lazy.of(() -> { + if (defaultTypingEnabled.get()) { + return null; + } + + return mapper.getDeserializationConfig().getDefaultTyper(null) + .buildTypeDeserializer(mapper.getDeserializationConfig(), mapper.getTypeFactory().constructType(Object.class), + Collections.emptyList()) + .getPropertyName(); + + }).or("@class"); + + typeResolver = new TypeResolver(Lazy.of(() -> mapper.getTypeFactory()), typeHintPropertyName); + } + /** * Creates {@link GenericJackson2JsonRedisSerializer} and configures {@link ObjectMapper} for default typing. */ @@ -71,6 +104,7 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer type) { + + if (internalReader || !type.equals(Object.class) || !defaultTypingEnabled.get()) { + return typeResolver.constructType(type); + } + + return typeResolver.resolveType(source, type); + } + + private static class TypeResolver { + + private final ObjectReader objectReader = new ObjectMapper().reader(); + + private final Supplier typeFactory; + private Supplier hintName; + + public TypeResolver(Supplier typeFactory, Supplier hintName) { + + this.typeFactory = typeFactory; + this.hintName = hintName; + } + + protected JavaType constructType(Class type) { + return typeFactory.get().constructType(type); + } + + protected JavaType resolveType(byte[] source, Class type) { + + try { + TextNode typeName = (TextNode) objectReader.readValue(source, JsonNode.class).get(hintName.get()); + if (typeName != null) { + return typeFactory.get().constructFromCanonical(typeName.textValue()); + } + } catch (IOException e) { + // TODO: logging? + } + + return constructType(type); + } + + } + /** * {@link StdSerializer} adding class information required by default typing. This allows de-/serialization of * {@link NullValue}. diff --git a/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java b/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java index 9535e7512..ae565f7ea 100644 --- a/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java @@ -21,12 +21,12 @@ import static org.springframework.test.util.ReflectionTestUtils.*; import static org.springframework.util.ObjectUtils.*; import lombok.Data; +import lombok.ToString; import java.io.IOException; import org.junit.jupiter.api.Test; import org.mockito.Mockito; - import org.springframework.beans.BeanUtils; import org.springframework.cache.support.NullValue; @@ -182,6 +182,34 @@ class GenericJackson2JsonRedisSerializerUnitTests { assertThat(new String(result)).contains("id").contains("name").doesNotContain("email"); } + @Test // GH-2322 + void shouldConsiderReader() { + + User user = new User(); + user.email = "walter@heisenberg.com"; + user.id = 42; + user.name = "Walter White"; + + GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer((String) null, + (mapper, source, type) -> { + if (type.getRawClass() == User.class) { + return mapper.readerWithView(Views.Basic.class).forType(type).readValue(source); + } + return mapper.readValue(source, type); + }, JacksonObjectWriter.create()); + + byte[] serializedValue = serializer.serialize(user); + + Object result = serializer.deserialize(serializedValue); + assertThat(result).isInstanceOf(User.class).satisfies(it -> { + User u = (User) it; + assertThat(u.id).isEqualTo(user.id); + assertThat(u.name).isEqualTo(user.name); + assertThat(u.email).isNull(); + assertThat(u.mobile).isNull(); + }); + } + private static void serializeAndDeserializeNullValue(GenericJackson2JsonRedisSerializer serializer) { NullValue nv = BeanUtils.instantiateClass(NullValue.class); @@ -272,14 +300,15 @@ class GenericJackson2JsonRedisSerializerUnitTests { } } - public class User { + @ToString + static class User { @JsonView(Views.Basic.class) public int id; @JsonView(Views.Basic.class) public String name; @JsonView(Views.Detailed.class) public String email; @JsonView(Views.Detailed.class) public String mobile; } - public class Views { + static class Views { interface Basic {} interface Detailed {}