diff --git a/pom.xml b/pom.xml
index 09bebeb5bd..bab11bf0cb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-redis
- 3.4.0-SNAPSHOT
+ 3.4.0-GH-2981-SNAPSHOT
Spring Data Redis
Spring Data module for Redis
@@ -276,6 +276,13 @@
test
+
+ org.msgpack
+ jackson-dataformat-msgpack
+ 0.9.8
+ test
+
+
edu.umd.cs.mtc
multithreadedtc
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 7ecc89d18c..dab4e7716f 100644
--- a/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java
+++ b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java
@@ -32,19 +32,26 @@
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.deser.BeanDeserializerFactory;
+import com.fasterxml.jackson.databind.deser.DefaultDeserializationContext;
+import com.fasterxml.jackson.databind.deser.std.JsonNodeDeserializer;
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.jsontype.impl.StdTypeResolverBuilder;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.TextNode;
+import com.fasterxml.jackson.databind.node.TreeTraversingParser;
import com.fasterxml.jackson.databind.ser.SerializerFactory;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.databind.type.TypeFactory;
@@ -179,7 +186,7 @@ private static TypeResolver newTypeResolver(ObjectMapper mapper, @Nullable Strin
Lazy lazyTypeHintPropertyName = typeHintPropertyName != null ? Lazy.of(typeHintPropertyName)
: newLazyTypeHintPropertyName(mapper, defaultTypingEnabled);
- return new TypeResolver(lazyTypeFactory, lazyTypeHintPropertyName);
+ return new TypeResolver(mapper, lazyTypeFactory, lazyTypeHintPropertyName);
}
private static Lazy newLazyTypeHintPropertyName(ObjectMapper mapper, Lazy defaultTypingEnabled) {
@@ -300,12 +307,21 @@ public T deserialize(@Nullable byte[] source, Class type) throws Serializ
}
try {
- return (T) reader.read(mapper, source, resolveType(source, type));
+
+ TypeTuple typeTuple = resolveType(source, type);
+ try (JsonParser parser = createParser(source, typeTuple)) {
+ return (T) reader.read(mapper, parser, typeTuple.type());
+ }
+
} catch (Exception ex) {
- throw new SerializationException("Could not read JSON:%s ".formatted(ex.getMessage()), ex);
+ throw new SerializationException("Could not read JSON: %s ".formatted(ex.getMessage()), ex);
}
}
+ private JsonParser createParser(byte[] source, TypeTuple typeTuple) throws IOException {
+ return typeTuple.node() == null ? mapper.createParser(source) : new TreeTraversingParser(typeTuple.node(), mapper);
+ }
+
/**
* Builder method used to configure and customize the internal Jackson {@link ObjectMapper} created by this
* {@link GenericJackson2JsonRedisSerializer} and used to de/serialize {@link Object objects} as {@literal JSON}.
@@ -326,28 +342,31 @@ public GenericJackson2JsonRedisSerializer configure(Consumer objec
return this;
}
- protected JavaType resolveType(byte[] source, Class> type) throws IOException {
+ protected TypeTuple resolveType(byte[] source, Class> type) throws IOException {
if (!type.equals(Object.class) || !defaultTypingEnabled.get()) {
- return typeResolver.constructType(type);
+ return new TypeTuple(typeResolver.constructType(type), null);
}
return typeResolver.resolveType(source, type);
}
+ protected record TypeTuple(JavaType type, @Nullable JsonNode node) {
+
+ }
+
/**
* @since 3.0
*/
static class TypeResolver {
- // need a separate instance to bypass class hint checks
- private final ObjectMapper mapper = new ObjectMapper();
-
+ private final ObjectMapper mapper;
private final Supplier typeFactory;
private final Supplier hintName;
- TypeResolver(Supplier typeFactory, Supplier hintName) {
+ TypeResolver(ObjectMapper mapper, Supplier typeFactory, Supplier hintName) {
+ this.mapper = mapper;
this.typeFactory = typeFactory;
this.hintName = hintName;
}
@@ -356,16 +375,52 @@ protected JavaType constructType(Class> type) {
return typeFactory.get().constructType(type);
}
- protected JavaType resolveType(byte[] source, Class> type) throws IOException {
+ protected TypeTuple resolveType(byte[] source, Class> type) throws IOException {
- JsonNode root = mapper.readTree(source);
+ JsonNode root = readTree(source);
JsonNode jsonNode = root.get(hintName.get());
if (jsonNode instanceof TextNode && jsonNode.asText() != null) {
- return typeFactory.get().constructFromCanonical(jsonNode.asText());
+ return new TypeTuple(typeFactory.get().constructFromCanonical(jsonNode.asText()), root);
}
- return constructType(type);
+ return new TypeTuple(constructType(type), root);
+ }
+
+ /**
+ * Lenient variant of ObjectMapper._readTreeAndClose using a strict {@link JsonNodeDeserializer}.
+ *
+ * @param source
+ * @return
+ * @throws IOException
+ */
+ private JsonNode readTree(byte[] source) throws IOException {
+
+ JsonDeserializer extends JsonNode> deserializer = JsonNodeDeserializer.getDeserializer(JsonNode.class);
+ DeserializationConfig cfg = mapper.getDeserializationConfig();
+
+ try (JsonParser parser = mapper.createParser(source)) {
+
+ cfg.initialize(parser);
+ JsonToken t = parser.currentToken();
+ if (t == null) {
+ t = parser.nextToken();
+ if (t == null) {
+ return cfg.getNodeFactory().missingNode();
+ }
+ }
+
+ /*
+ * Hokey pokey! Oh my.
+ */
+ DefaultDeserializationContext ctxt = new DefaultDeserializationContext.Impl(BeanDeserializerFactory.instance)
+ .createInstance(cfg, parser, mapper.getInjectableValues());
+ if (t == JsonToken.VALUE_NULL) {
+ return cfg.getNodeFactory().nullNode();
+ } else {
+ return deserializer.deserialize(parser, ctxt);
+ }
+ }
}
}
diff --git a/src/main/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializer.java
index 76238bc60d..2d68ea5aed 100644
--- a/src/main/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializer.java
+++ b/src/main/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializer.java
@@ -166,7 +166,7 @@ public T deserialize(@Nullable byte[] bytes) throws SerializationException {
return null;
}
try {
- return (T) this.reader.read(this.mapper, bytes, javaType);
+ return (T) this.reader.read(this.mapper, this.mapper.createParser(bytes), javaType);
} catch (Exception ex) {
throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex);
}
diff --git a/src/main/java/org/springframework/data/redis/serializer/JacksonObjectReader.java b/src/main/java/org/springframework/data/redis/serializer/JacksonObjectReader.java
index 4e76cd4ba7..5070d5c4b5 100644
--- a/src/main/java/org/springframework/data/redis/serializer/JacksonObjectReader.java
+++ b/src/main/java/org/springframework/data/redis/serializer/JacksonObjectReader.java
@@ -18,6 +18,7 @@
import java.io.IOException;
import java.io.InputStream;
+import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -43,7 +44,20 @@ public interface JacksonObjectReader {
* @return the deserialized Java object.
* @throws IOException if an I/O error or JSON deserialization error occurs.
*/
- Object read(ObjectMapper mapper, byte[] source, JavaType type) throws IOException;
+ default Object read(ObjectMapper mapper, byte[] source, JavaType type) throws IOException {
+ return read(mapper, mapper.createParser(source), type);
+ }
+
+ /**
+ * Read an object graph from the given root JSON into a Java object considering the {@link JavaType}.
+ *
+ * @param mapper the object mapper to use.
+ * @param parser the JSON parser to use.
+ * @param type the Java target type
+ * @return the deserialized Java object.
+ * @throws IOException if an I/O error or JSON deserialization error occurs.
+ */
+ Object read(ObjectMapper mapper, JsonParser parser, JavaType type) throws IOException;
/**
* Create a default {@link JacksonObjectReader} delegating to {@link ObjectMapper#readValue(InputStream, JavaType)}.
@@ -51,7 +65,7 @@ public interface JacksonObjectReader {
* @return the default {@link JacksonObjectReader}.
*/
static JacksonObjectReader create() {
- return (mapper, source, type) -> mapper.readValue(source, 0, source.length, type);
+ return ObjectMapper::readValue;
}
}
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 16daa677c8..c91e6b46f7 100644
--- a/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java
+++ b/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java
@@ -32,6 +32,7 @@
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
+import org.msgpack.jackson.dataformat.MessagePackFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.cache.support.NullValue;
@@ -449,6 +450,7 @@ void configureWithNullConsumerThrowsIllegalArgumentException() {
@Test
void defaultSerializeAndDeserializeNullValueWithBuilderClass() {
+
GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder()
.objectMapper(new ObjectMapper().enableDefaultTyping(DefaultTyping.EVERYTHING, As.PROPERTY))
.build();
@@ -487,6 +489,31 @@ public void serializeWithType(NullValue value, JsonGenerator jsonGenerator, Seri
assertThat(deserializedValue).isNull();
}
+ @Test // GH-2981
+ void defaultSerializeAndDeserializeWithCustomJsonFactory() {
+
+ GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder()
+ .objectMapper(
+ new ObjectMapper(new MessagePackFactory()).enableDefaultTyping(DefaultTyping.EVERYTHING, As.PROPERTY))
+ .build();
+
+ byte[] serializedValue = serializer.serialize(COMPLEX_OBJECT);
+
+ Object deserializedValue = serializer.deserialize(serializedValue, Object.class);
+ assertThat(deserializedValue).isEqualTo(COMPLEX_OBJECT);
+ }
+
+ @Test // GH-2981
+ void defaultSerializeAndDeserializeNullValueWithBuilderClassAndCustomJsonFactory() {
+
+ GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder()
+ .objectMapper(
+ new ObjectMapper(new MessagePackFactory()).enableDefaultTyping(DefaultTyping.EVERYTHING, As.PROPERTY))
+ .build();
+
+ serializeAndDeserializeNullValue(serializer);
+ }
+
private static void serializeAndDeserializeNullValue(GenericJackson2JsonRedisSerializer serializer) {
NullValue nv = BeanUtils.instantiateClass(NullValue.class);