diff --git a/pom.xml b/pom.xml
index b108ed6774..6c0b05a01e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-redis
- 2.7.2-SNAPSHOT
+ 2.7.2-GH-SNAPSHOT
Spring Data Redis
Spring Data module for Redis
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 b1296caedb..1234449a32 100644
--- a/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java
+++ b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java
@@ -20,17 +20,21 @@
import org.springframework.cache.support.NullValue;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
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.core.JsonProcessingException;
+import com.fasterxml.jackson.core.TreeNode;
+import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
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.jsontype.impl.StdTypeResolverBuilder;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.SerializerFactory;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
@@ -71,12 +75,15 @@ public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName
// the type hint embedded for deserialization using the default typing feature.
registerNullValueSerializer(mapper, classPropertyTypeName);
+ StdTypeResolverBuilder typer = new TypeResolverBuilder(DefaultTyping.EVERYTHING,
+ mapper.getPolymorphicTypeValidator());
+ typer = typer.init(JsonTypeInfo.Id.CLASS, null);
+ typer = typer.inclusion(JsonTypeInfo.As.PROPERTY);
+
if (StringUtils.hasText(classPropertyTypeName)) {
- mapper.activateDefaultTypingAsProperty(mapper.getPolymorphicTypeValidator(), DefaultTyping.EVERYTHING,
- classPropertyTypeName);
- } else {
- mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(), DefaultTyping.EVERYTHING, As.PROPERTY);
+ typer = typer.typeProperty(classPropertyTypeName);
}
+ mapper.setDefaultTyping(typer);
}
/**
@@ -184,8 +191,7 @@ private static class NullValueSerializer extends StdSerializer {
* @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider)
*/
@Override
- public void serialize(NullValue value, JsonGenerator jgen, SerializerProvider provider)
- throws IOException {
+ public void serialize(NullValue value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeStartObject();
jgen.writeStringField(classIdentifier, NullValue.class.getName());
@@ -198,4 +204,64 @@ public void serializeWithType(NullValue value, JsonGenerator gen, SerializerProv
serialize(value, gen, serializers);
}
}
+
+ /**
+ * Custom {@link StdTypeResolverBuilder} that considers typing for non-primitive types. Primitives, their wrappers and
+ * primitive arrays do not require type hints. The default {@code DefaultTyping#EVERYTHING} typing does not satisfy
+ * those requirements.
+ *
+ * @author Mark Paluch
+ * @since 2.7.2
+ */
+ private static class TypeResolverBuilder extends ObjectMapper.DefaultTypeResolverBuilder {
+
+ public TypeResolverBuilder(DefaultTyping t, PolymorphicTypeValidator ptv) {
+ super(t, ptv);
+ }
+
+ @Override
+ public ObjectMapper.DefaultTypeResolverBuilder withDefaultImpl(Class> defaultImpl) {
+ return this;
+ }
+
+ /**
+ * Method called to check if the default type handler should be used for given type. Note: "natural types" (String,
+ * Boolean, Integer, Double) will never use typing; that is both due to them being concrete and final, and since
+ * actual serializers and deserializers will also ignore any attempts to enforce typing.
+ */
+ public boolean useForType(JavaType t) {
+
+ if (t.isJavaLangObject()) {
+ return true;
+ }
+
+ t = resolveArrayOrWrapper(t);
+
+ if (ClassUtils.isPrimitiveOrWrapper(t.getRawClass())) {
+ return false;
+ }
+
+ // [databind#88] Should not apply to JSON tree models:
+ return !TreeNode.class.isAssignableFrom(t.getRawClass());
+ }
+
+ private JavaType resolveArrayOrWrapper(JavaType type) {
+
+ while (type.isArrayType()) {
+ type = type.getContentType();
+ if (type.isReferenceType()) {
+ type = resolveArrayOrWrapper(type);
+ }
+ }
+
+ while (type.isReferenceType()) {
+ type = type.getReferencedType();
+ if (type.isArrayType()) {
+ type = resolveArrayOrWrapper(type);
+ }
+ }
+
+ return type;
+ }
+ }
}
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 f13d7cf1a8..0f698c2a69 100644
--- a/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java
+++ b/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java
@@ -23,6 +23,7 @@
import lombok.Data;
import java.io.IOException;
+import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@@ -157,9 +158,107 @@ void deserializeShouldBeAbleToRestoreFinalObjectAfterSerialization() {
FinalObject source = new FinalObject();
source.longValue = 1L;
+ source.myArray = new int[] { 1, 2, 3 };
source.simpleObject = new SimpleObject(2L);
assertThat(serializer.deserialize(serializer.serialize(source))).isEqualTo(source);
+ assertThat(serializer.deserialize(
+ ("{\"@class\":\"org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializerUnitTests$FinalObject\",\"longValue\":1,\"myArray\":[1,2,3],\n"
+ + "\"simpleObject\":{\"@class\":\"org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializerUnitTests$SimpleObject\",\"longValue\":2}}")
+ .getBytes())).isEqualTo(source);
+ }
+
+ @Test // GH-2361
+ void shouldDeserializePrimitiveArrayWithoutTypeHint() {
+
+ GenericJackson2JsonRedisSerializer gs = new GenericJackson2JsonRedisSerializer();
+ CountAndArray result = (CountAndArray) gs.deserialize(
+ ("{\"@class\":\"org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializerUnitTests$CountAndArray\", \"count\":1, \"available\":[0,1]}")
+ .getBytes());
+
+ assertThat(result.getCount()).isEqualTo(1);
+ assertThat(result.getAvailable()).containsExactly(0, 1);
+ }
+
+ @Test // GH-2361
+ void shouldDeserializePrimitiveWrapperArrayWithoutTypeHint() {
+
+ GenericJackson2JsonRedisSerializer gs = new GenericJackson2JsonRedisSerializer();
+ CountAndArray result = (CountAndArray) gs.deserialize(
+ ("{\"@class\":\"org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializerUnitTests$CountAndArray\", \"count\":1, \"arrayOfPrimitiveWrapper\":[0,1]}")
+ .getBytes());
+
+ assertThat(result.getCount()).isEqualTo(1);
+ assertThat(result.getArrayOfPrimitiveWrapper()).containsExactly(0L, 1L);
+ }
+
+ @Test // GH-2361
+ void doesNotIncludeTypingForPrimitiveArrayWrappers() {
+
+ GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
+
+ WithWrapperTypes source = new WithWrapperTypes();
+ source.primitiveWrapper = new AtomicReference<>();
+ source.primitiveArrayWrapper = new AtomicReference<>(new Integer[] { 200, 300 });
+ source.simpleObjectWrapper = new AtomicReference<>();
+
+ byte[] serializedValue = serializer.serialize(source);
+
+ assertThat(new String(serializedValue)) //
+ .contains("\"primitiveArrayWrapper\":[200,300]") //
+ .doesNotContain("\"[Ljava.lang.Integer;\"");
+
+ assertThat(serializer.deserialize(serializedValue)) //
+ .isInstanceOf(WithWrapperTypes.class) //
+ .satisfies(it -> {
+ WithWrapperTypes deserialized = (WithWrapperTypes) it;
+ assertThat(deserialized.primitiveArrayWrapper).hasValue(source.primitiveArrayWrapper.get());
+ });
+ }
+
+ @Test // GH-2361
+ void doesNotIncludeTypingForPrimitiveWrappers() {
+
+ GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
+
+ WithWrapperTypes source = new WithWrapperTypes();
+ source.primitiveWrapper = new AtomicReference<>(123L);
+
+ byte[] serializedValue = serializer.serialize(source);
+
+ assertThat(new String(serializedValue)) //
+ .contains("\"primitiveWrapper\":123") //
+ .doesNotContain("\"Ljava.lang.Long;\"");
+
+ assertThat(serializer.deserialize(serializedValue)) //
+ .isInstanceOf(WithWrapperTypes.class) //
+ .satisfies(it -> {
+ WithWrapperTypes deserialized = (WithWrapperTypes) it;
+ assertThat(deserialized.primitiveWrapper).hasValue(source.primitiveWrapper.get());
+ });
+ }
+
+ @Test // GH-2361
+ void includesTypingForWrappedObjectTypes() {
+
+ GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
+
+ SimpleObject simpleObject = new SimpleObject(100L);
+ WithWrapperTypes source = new WithWrapperTypes();
+ source.simpleObjectWrapper = new AtomicReference<>(simpleObject);
+
+ byte[] serializedValue = serializer.serialize(source);
+
+ assertThat(new String(serializedValue)) //
+ .contains(
+ "\"simpleObjectWrapper\":{\"@class\":\"org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializerUnitTests$SimpleObject\",\"longValue\":100}");
+
+ assertThat(serializer.deserialize(serializedValue)) //
+ .isInstanceOf(WithWrapperTypes.class) //
+ .satisfies(it -> {
+ WithWrapperTypes deserialized = (WithWrapperTypes) it;
+ assertThat(deserialized.simpleObjectWrapper).hasValue(source.simpleObjectWrapper.get());
+ });
}
private static void serializeAndDeserializeNullValue(GenericJackson2JsonRedisSerializer serializer) {
@@ -211,12 +310,12 @@ public boolean equals(Object obj) {
return nullSafeEquals(this.stringValue, other.stringValue)
&& nullSafeEquals(this.simpleObject, other.simpleObject);
}
-
}
@Data
static final class FinalObject {
public Long longValue;
+ public int[] myArray;
SimpleObject simpleObject;
}
@@ -252,4 +351,19 @@ public boolean equals(Object obj) {
}
}
+ @Data
+ static class CountAndArray {
+
+ private int count;
+ private int[] available;
+ private Long[] arrayOfPrimitiveWrapper;
+ }
+
+ @Data
+ static class WithWrapperTypes {
+
+ AtomicReference primitiveWrapper;
+ AtomicReference primitiveArrayWrapper;
+ AtomicReference simpleObjectWrapper;
+ }
}