Skip to content

Commit b366c8c

Browse files
Reader should provide target Object type to allow easy customization.
This commit makes sure to extract and pass on the target type, otherwise it would be Object.class all the time.
1 parent c2b5517 commit b366c8c

File tree

2 files changed

+115
-5
lines changed

2 files changed

+115
-5
lines changed

src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java

+83-2
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,31 @@
1616
package org.springframework.data.redis.serializer;
1717

1818
import java.io.IOException;
19+
import java.util.Collections;
20+
import java.util.function.Supplier;
1921

2022
import org.springframework.cache.support.NullValue;
23+
import org.springframework.data.util.Lazy;
2124
import org.springframework.lang.Nullable;
2225
import org.springframework.util.Assert;
2326
import org.springframework.util.StringUtils;
2427

2528
import com.fasterxml.jackson.annotation.JsonTypeInfo;
2629
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
2730
import com.fasterxml.jackson.core.JsonGenerator;
31+
import com.fasterxml.jackson.databind.JavaType;
32+
import com.fasterxml.jackson.databind.JsonNode;
2833
import com.fasterxml.jackson.databind.ObjectMapper;
2934
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
35+
import com.fasterxml.jackson.databind.ObjectReader;
3036
import com.fasterxml.jackson.databind.SerializerProvider;
3137
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
3238
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
3339
import com.fasterxml.jackson.databind.module.SimpleModule;
40+
import com.fasterxml.jackson.databind.node.TextNode;
3441
import com.fasterxml.jackson.databind.ser.SerializerFactory;
3542
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
43+
import com.fasterxml.jackson.databind.type.TypeFactory;
3644

3745
/**
3846
* Generic Jackson 2-based {@link RedisSerializer} that maps {@link Object objects} to JSON using dynamic typing.
@@ -47,12 +55,37 @@
4755
*/
4856
public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Object> {
4957

50-
private final ObjectMapper mapper;
58+
private ObjectMapper mapper;
5159

5260
private final JacksonObjectReader reader;
5361

5462
private final JacksonObjectWriter writer;
5563

64+
private boolean internalReader = false;
65+
66+
private final TypeResolver typeResolver;
67+
68+
private Lazy<Boolean> defaultTypingEnabled = Lazy
69+
.of(() -> mapper.getSerializationConfig().getDefaultTyper(null) != null);
70+
71+
private Lazy<String> typeHintPropertyName;
72+
73+
{
74+
typeHintPropertyName = Lazy.of(() -> {
75+
if (defaultTypingEnabled.get()) {
76+
return null;
77+
}
78+
79+
return mapper.getDeserializationConfig().getDefaultTyper(null)
80+
.buildTypeDeserializer(mapper.getDeserializationConfig(), mapper.getTypeFactory().constructType(Object.class),
81+
Collections.emptyList())
82+
.getPropertyName();
83+
84+
}).or("@class");
85+
86+
typeResolver = new TypeResolver(Lazy.of(() -> mapper.getTypeFactory()), typeHintPropertyName);
87+
}
88+
5689
/**
5790
* Creates {@link GenericJackson2JsonRedisSerializer} and configures {@link ObjectMapper} for default typing.
5891
*/
@@ -71,6 +104,7 @@ public GenericJackson2JsonRedisSerializer() {
71104
*/
72105
public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName) {
73106
this(classPropertyTypeName, JacksonObjectReader.create(), JacksonObjectWriter.create());
107+
this.internalReader = true;
74108
}
75109

76110
/**
@@ -100,6 +134,10 @@ public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName
100134
} else {
101135
mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(), DefaultTyping.EVERYTHING, As.PROPERTY);
102136
}
137+
138+
if (classPropertyTypeName != null) {
139+
typeHintPropertyName = Lazy.of(classPropertyTypeName);
140+
}
103141
}
104142

105143
/**
@@ -111,6 +149,7 @@ public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName
111149
*/
112150
public GenericJackson2JsonRedisSerializer(ObjectMapper mapper) {
113151
this(mapper, JacksonObjectReader.create(), JacksonObjectWriter.create());
152+
this.internalReader = true;
114153
}
115154

116155
/**
@@ -188,12 +227,54 @@ public <T> T deserialize(@Nullable byte[] source, Class<T> type) throws Serializ
188227
}
189228

190229
try {
191-
return (T) reader.read(mapper, source, mapper.getTypeFactory().constructType(type));
230+
return (T) reader.read(mapper, source, resolveType(source, type));
192231
} catch (Exception ex) {
193232
throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex);
194233
}
195234
}
196235

236+
protected JavaType resolveType(byte[] source, Class<?> type) {
237+
238+
if (internalReader || !type.equals(Object.class) || !defaultTypingEnabled.get()) {
239+
return typeResolver.constructType(type);
240+
}
241+
242+
return typeResolver.resolveType(source, type);
243+
}
244+
245+
private static class TypeResolver {
246+
247+
private final ObjectReader objectReader = new ObjectMapper().reader();
248+
249+
private final Supplier<TypeFactory> typeFactory;
250+
private Supplier<String> hintName;
251+
252+
public TypeResolver(Supplier<TypeFactory> typeFactory, Supplier<String> hintName) {
253+
254+
this.typeFactory = typeFactory;
255+
this.hintName = hintName;
256+
}
257+
258+
protected JavaType constructType(Class<?> type) {
259+
return typeFactory.get().constructType(type);
260+
}
261+
262+
protected JavaType resolveType(byte[] source, Class<?> type) {
263+
264+
try {
265+
TextNode typeName = (TextNode) objectReader.readValue(source, JsonNode.class).get(hintName.get());
266+
if (typeName != null) {
267+
return typeFactory.get().constructFromCanonical(typeName.textValue());
268+
}
269+
} catch (IOException e) {
270+
// TODO: logging?
271+
}
272+
273+
return constructType(type);
274+
}
275+
276+
}
277+
197278
/**
198279
* {@link StdSerializer} adding class information required by default typing. This allows de-/serialization of
199280
* {@link NullValue}.

src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java

+32-3
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@
2121
import static org.springframework.util.ObjectUtils.*;
2222

2323
import lombok.Data;
24+
import lombok.ToString;
2425

2526
import java.io.IOException;
2627

2728
import org.junit.jupiter.api.Test;
2829
import org.mockito.Mockito;
29-
3030
import org.springframework.beans.BeanUtils;
3131
import org.springframework.cache.support.NullValue;
3232

@@ -182,6 +182,34 @@ void shouldConsiderWriter() {
182182
assertThat(new String(result)).contains("id").contains("name").doesNotContain("email");
183183
}
184184

185+
@Test // GH-2322
186+
void shouldConsiderReader() {
187+
188+
User user = new User();
189+
user.email = "[email protected]";
190+
user.id = 42;
191+
user.name = "Walter White";
192+
193+
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer((String) null,
194+
(mapper, source, type) -> {
195+
if (type.getRawClass() == User.class) {
196+
return mapper.readerWithView(Views.Basic.class).forType(type).readValue(source);
197+
}
198+
return mapper.readValue(source, type);
199+
}, JacksonObjectWriter.create());
200+
201+
byte[] serializedValue = serializer.serialize(user);
202+
203+
Object result = serializer.deserialize(serializedValue);
204+
assertThat(result).isInstanceOf(User.class).satisfies(it -> {
205+
User u = (User) it;
206+
assertThat(u.id).isEqualTo(user.id);
207+
assertThat(u.name).isEqualTo(user.name);
208+
assertThat(u.email).isNull();
209+
assertThat(u.mobile).isNull();
210+
});
211+
}
212+
185213
private static void serializeAndDeserializeNullValue(GenericJackson2JsonRedisSerializer serializer) {
186214

187215
NullValue nv = BeanUtils.instantiateClass(NullValue.class);
@@ -272,14 +300,15 @@ public boolean equals(Object obj) {
272300
}
273301
}
274302

275-
public class User {
303+
@ToString
304+
static class User {
276305
@JsonView(Views.Basic.class) public int id;
277306
@JsonView(Views.Basic.class) public String name;
278307
@JsonView(Views.Detailed.class) public String email;
279308
@JsonView(Views.Detailed.class) public String mobile;
280309
}
281310

282-
public class Views {
311+
static class Views {
283312
interface Basic {}
284313

285314
interface Detailed {}

0 commit comments

Comments
 (0)