Support deserialization in GenericJackson2JsonRedisSerializer when using custom JsonFactory.

Closes: #2981
Original Pull Request: #2999
This commit is contained in:
Mark Paluch
2024-09-16 14:35:10 +02:00
committed by Christoph Strobl
parent f2752d1b02
commit 8c74d78872
3 changed files with 81 additions and 6 deletions

View File

@@ -276,6 +276,13 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.msgpack</groupId>
<artifactId>jackson-dataformat-msgpack</artifactId>
<version>0.9.8</version>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>edu.umd.cs.mtc</groupId> <groupId>edu.umd.cs.mtc</groupId>
<artifactId>multithreadedtc</artifactId> <artifactId>multithreadedtc</artifactId>

View File

@@ -32,13 +32,19 @@ import org.springframework.util.StringUtils;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As; import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import com.fasterxml.jackson.core.JsonGenerator; 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.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationConfig; import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
import com.fasterxml.jackson.databind.SerializerProvider; 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.PolymorphicTypeValidator;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer; import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
@@ -179,7 +185,7 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
Lazy<String> lazyTypeHintPropertyName = typeHintPropertyName != null ? Lazy.of(typeHintPropertyName) Lazy<String> lazyTypeHintPropertyName = typeHintPropertyName != null ? Lazy.of(typeHintPropertyName)
: newLazyTypeHintPropertyName(mapper, defaultTypingEnabled); : newLazyTypeHintPropertyName(mapper, defaultTypingEnabled);
return new TypeResolver(lazyTypeFactory, lazyTypeHintPropertyName); return new TypeResolver(mapper, lazyTypeFactory, lazyTypeHintPropertyName);
} }
private static Lazy<String> newLazyTypeHintPropertyName(ObjectMapper mapper, Lazy<Boolean> defaultTypingEnabled) { private static Lazy<String> newLazyTypeHintPropertyName(ObjectMapper mapper, Lazy<Boolean> defaultTypingEnabled) {
@@ -340,14 +346,13 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
*/ */
static class TypeResolver { static class TypeResolver {
// need a separate instance to bypass class hint checks private final ObjectMapper mapper;
private final ObjectMapper mapper = new ObjectMapper();
private final Supplier<TypeFactory> typeFactory; private final Supplier<TypeFactory> typeFactory;
private final Supplier<String> hintName; 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.typeFactory = typeFactory;
this.hintName = hintName; this.hintName = hintName;
} }
@@ -358,7 +363,7 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
protected JavaType resolveType(byte[] source, Class<?> type) throws IOException { protected JavaType resolveType(byte[] source, Class<?> type) throws IOException {
JsonNode root = mapper.readTree(source); JsonNode root = readTree(source);
JsonNode jsonNode = root.get(hintName.get()); JsonNode jsonNode = root.get(hintName.get());
if (jsonNode instanceof TextNode && jsonNode.asText() != null) { if (jsonNode instanceof TextNode && jsonNode.asText() != null) {
@@ -367,6 +372,42 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
return constructType(type); return constructType(type);
} }
/**
* 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);
}
}
}
} }
/** /**

View File

@@ -32,6 +32,7 @@ import java.util.function.Consumer;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.msgpack.jackson.dataformat.MessagePackFactory;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.cache.support.NullValue; import org.springframework.cache.support.NullValue;
@@ -449,6 +450,7 @@ class GenericJackson2JsonRedisSerializerUnitTests {
@Test @Test
void defaultSerializeAndDeserializeNullValueWithBuilderClass() { void defaultSerializeAndDeserializeNullValueWithBuilderClass() {
GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder()
.objectMapper(new ObjectMapper().enableDefaultTyping(DefaultTyping.EVERYTHING, As.PROPERTY)) .objectMapper(new ObjectMapper().enableDefaultTyping(DefaultTyping.EVERYTHING, As.PROPERTY))
.build(); .build();
@@ -487,6 +489,31 @@ class GenericJackson2JsonRedisSerializerUnitTests {
assertThat(deserializedValue).isNull(); 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) { private static void serializeAndDeserializeNullValue(GenericJackson2JsonRedisSerializer serializer) {
NullValue nv = BeanUtils.instantiateClass(NullValue.class); NullValue nv = BeanUtils.instantiateClass(NullValue.class);