diff --git a/src/main/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializer.java index e486a3f7a5..99aecbd35a 100644 --- a/src/main/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializer.java +++ b/src/main/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializer.java @@ -23,7 +23,6 @@ import tools.jackson.core.Version; import tools.jackson.databind.DefaultTyping; import tools.jackson.databind.DeserializationConfig; -import tools.jackson.databind.DeserializationContext; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.JacksonModule; import tools.jackson.databind.JavaType; @@ -31,6 +30,7 @@ import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.SerializationContext; import tools.jackson.databind.cfg.MapperBuilder; +import tools.jackson.databind.deser.DeserializationContextExt; import tools.jackson.databind.deser.jackson.BaseNodeDeserializer; import tools.jackson.databind.deser.jackson.JsonNodeDeserializer; import tools.jackson.databind.json.JsonMapper; @@ -68,6 +68,7 @@ * {@link JacksonObjectWriter}. * * @author Christoph Strobl + * @author Tomaz Cerar * @see JacksonObjectReader * @see JacksonObjectWriter * @see ObjectMapper @@ -503,10 +504,12 @@ private JsonNode readTree(byte[] source) throws IOException { } } - /* - * Hokey pokey! Oh my. - */ - DeserializationContext ctxt = mapper._deserializationContext(); + // Bind the parser to the context so its stream-read capabilities are initialized: the node + // deserializer consults them (e.g. StreamReadCapability.DUPLICATE_PROPERTIES when a duplicate + // field name is encountered) and would otherwise throw a NullPointerException. This mirrors the + // way ObjectMapper._readTreeAndClose(...) binds the parser via assignAndReturnParser(...). + DeserializationContextExt ctxt = mapper._deserializationContext(); + ctxt.assignParser(parser); if (t == JsonToken.VALUE_NULL) { return cfg.getNodeFactory().nullNode(); diff --git a/src/test/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializerUnitTests.java b/src/test/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializerUnitTests.java index a3cec6c3d4..adef785884 100644 --- a/src/test/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializerUnitTests.java @@ -17,6 +17,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; +import static org.assertj.core.api.InstanceOfAssertFactories.map; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import static org.springframework.test.util.ReflectionTestUtils.*; @@ -60,6 +61,7 @@ * @author Christoph Strobl * @author Mark Paluch * @author John Blum + * @author Tomaz Cerar */ class GenericJacksonJsonRedisSerializerUnitTests { @@ -178,6 +180,19 @@ void shouldDeserializePrimitiveArrayWithoutTypeHint() { assertThat(result.getAvailable()).containsExactly(0, 1); } + @Test // GH-3396 + void deserializeShouldHandleDuplicatePropertyWithDefaultTyping() { + + // Duplicate property names are valid JSON (RFC 8259) and can appear in cached values written by an + // earlier serializer (e.g. Jackson 2). Resolving the type hint scans the JSON tree, which must not + // fail on a duplicate field (previously a NullPointerException from an unbound DeserializationContext). + byte[] source = "{\"@class\":\"java.util.LinkedHashMap\",\"a\":1,\"a\":2}".getBytes(StandardCharsets.UTF_8); + + Object result = serializer.deserialize(source); + + assertThat(result).asInstanceOf(map(String.class, Object.class)).containsEntry("a", 2); + } + @Test // GH-2322 void readsToMapForNonDefaultTyping() {