From 3f96d6ea3135cb43e70ec96e7b6e6396a6b9f067 Mon Sep 17 00:00:00 2001 From: Tomaz Cerar Date: Fri, 10 Jul 2026 15:19:31 +0200 Subject: [PATCH] Fix NullPointerException resolving type hints for JSON with duplicate properties. GenericJacksonJsonRedisSerializer.TypeResolver.readTree(...) built the JSON node tree using a DeserializationContext from ObjectMapper._deserializationContext() that was never bound to the parser, so its stream-read capabilities stayed uninitialized. Deserializing any value whose JSON contains a duplicate property then failed with a NullPointerException in BaseNodeDeserializer._handleDuplicateProperty, which consults StreamReadCapability.DUPLICATE_PROPERTIES. Duplicate property names are valid JSON (RFC 8259) and occur in practice, for example in values written by the Jackson 2 serializer. Bind the parser to the context (as ObjectMapper._readTreeAndClose does via assignAndReturnParser) so the type-hint tree scan handles them. Closes #3396 Signed-off-by: Tomaz Cerar --- .../GenericJacksonJsonRedisSerializer.java | 13 ++++++++----- ...enericJacksonJsonRedisSerializerUnitTests.java | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) 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() {