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; + } }