Skip to content

Introduce custom StdTypeResolverBuilder to support primitive arrays without type hints. #2364

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.7.2-SNAPSHOT</version>
<version>2.7.2-GH-SNAPSHOT</version>

<name>Spring Data Redis</name>
<description>Spring Data module for Redis</description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -184,8 +191,7 @@ private static class NullValueSerializer extends StdSerializer<NullValue> {
* @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());
Expand All @@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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<Long> primitiveWrapper;
AtomicReference<Integer[]> primitiveArrayWrapper;
AtomicReference<SimpleObject> simpleObjectWrapper;
}
}