Skip to content
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

Support deserialization in GenericJackson2JsonRedisSerializer when using custom JsonFactory #2999

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
9 changes: 8 additions & 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>3.4.0-SNAPSHOT</version>
<version>3.4.0-GH-2981-SNAPSHOT</version>

<name>Spring Data Redis</name>
<description>Spring Data module for Redis</description>
Expand Down Expand Up @@ -276,6 +276,13 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.msgpack</groupId>
<artifactId>jackson-dataformat-msgpack</artifactId>
<version>0.9.8</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>edu.umd.cs.mtc</groupId>
<artifactId>multithreadedtc</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -179,7 +186,7 @@ private static TypeResolver newTypeResolver(ObjectMapper mapper, @Nullable Strin
Lazy<String> lazyTypeHintPropertyName = typeHintPropertyName != null ? Lazy.of(typeHintPropertyName)
: newLazyTypeHintPropertyName(mapper, defaultTypingEnabled);

return new TypeResolver(lazyTypeFactory, lazyTypeHintPropertyName);
return new TypeResolver(mapper, lazyTypeFactory, lazyTypeHintPropertyName);
}

private static Lazy<String> newLazyTypeHintPropertyName(ObjectMapper mapper, Lazy<Boolean> defaultTypingEnabled) {
Expand Down Expand Up @@ -300,12 +307,21 @@ public <T> T deserialize(@Nullable byte[] source, Class<T> 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}.
Expand All @@ -326,28 +342,31 @@ public GenericJackson2JsonRedisSerializer configure(Consumer<ObjectMapper> 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> typeFactory;
private final Supplier<String> hintName;

TypeResolver(Supplier<TypeFactory> typeFactory, Supplier<String> hintName) {
TypeResolver(ObjectMapper mapper, Supplier<TypeFactory> typeFactory, Supplier<String> hintName) {

this.mapper = mapper;
this.typeFactory = typeFactory;
this.hintName = hintName;
}
Expand All @@ -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);
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -43,15 +44,28 @@ 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)}.
*
* @return the default {@link JacksonObjectReader}.
*/
static JacksonObjectReader create() {
return (mapper, source, type) -> mapper.readValue(source, 0, source.length, type);
return ObjectMapper::readValue;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -449,6 +450,7 @@ void configureWithNullConsumerThrowsIllegalArgumentException() {

@Test
void defaultSerializeAndDeserializeNullValueWithBuilderClass() {

GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder()
.objectMapper(new ObjectMapper().enableDefaultTyping(DefaultTyping.EVERYTHING, As.PROPERTY))
.build();
Expand Down Expand Up @@ -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);
Expand Down