Summary
GenericJacksonJsonRedisSerializer (the Jackson 3 serializer, with default typing enabled) throws a NullPointerException when deserializing any JSON object that contains a duplicate property name. The failure is in GenericJacksonJsonRedisSerializer.TypeResolver.readTree(byte[]), which builds a JsonNode tree using a DeserializationContext obtained from mapper._deserializationContext() — a context that is never bound to the parser, so its _readCapabilities is null. As soon as the tree contains a duplicate key, BaseNodeDeserializer._handleDuplicateProperty calls ctxt.isEnabled(StreamReadCapability.DUPLICATE_PROPERTIES) and NPEs.
Duplicate names are permitted by JSON (RFC 8259) and Jackson's own tree deserializer normally handles them gracefully (last value wins, unless FAIL_ON_READING_DUP_TREE_KEY is set). Here they crash instead.
Affected versions
- spring-data-redis 4.1.0 (Spring Data BOM 2026.0.0), Jackson 3.2.0 (
tools.jackson).
- The offending code is unchanged on
main (TypeResolver.readTree still uses mapper._deserializationContext() directly).
Minimal reproduction
No custom mapper, no special types — a default-typing serializer and a two-line JSON with a duplicate key:
import org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer;
import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
var validator = BasicPolymorphicTypeValidator.builder().allowIfSubType(Object.class).build();
var serializer = GenericJacksonJsonRedisSerializer.builder()
.enableDefaultTyping(validator)
.build();
// Any JSON object with a duplicate property name triggers it:
serializer.deserialize("{\"a\":1,\"a\":2}".getBytes());
Actual behaviour
org.springframework.data.redis.serializer.SerializationException: Could not read JSON:Cannot invoke
"tools.jackson.core.util.JacksonFeatureSet.isEnabled(tools.jackson.core.util.JacksonFeature)" because "this._readCapabilities" is null
Caused by: java.lang.NullPointerException: Cannot invoke
"tools.jackson.core.util.JacksonFeatureSet.isEnabled(tools.jackson.core.util.JacksonFeature)" because "this._readCapabilities" is null
at tools.jackson.databind.DeserializationContext.isEnabled(DeserializationContext.java:470)
at tools.jackson.databind.deser.jackson.BaseNodeDeserializer._handleDuplicateProperty(BaseNodeDeserializer.java:148)
at tools.jackson.databind.deser.jackson.BaseNodeDeserializer._deserializeContainerNoRecursion(BaseNodeDeserializer.java:375)
at tools.jackson.databind.deser.jackson.JsonNodeDeserializer.deserialize(JsonNodeDeserializer.java:94)
at tools.jackson.databind.deser.jackson.JsonNodeDeserializer.deserialize(JsonNodeDeserializer.java:12)
at org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer$TypeResolver.readTree(GenericJacksonJsonRedisSerializer.java:514)
at org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer$TypeResolver.resolveType(GenericJacksonJsonRedisSerializer.java:478)
at org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer.resolveType(GenericJacksonJsonRedisSerializer.java:222)
at org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer.deserialize(GenericJacksonJsonRedisSerializer.java:210)
Expected behaviour
The value should deserialize the same way Jackson's tree reader would (last value wins by default), or at least raise a meaningful SerializationException — not a NullPointerException from an uninitialized DeserializationContext.
Root cause
TypeResolver.readTree(byte[]):
private JsonNode readTree(byte[] source) throws IOException {
BaseNodeDeserializer<?> deserializer = JsonNodeDeserializer.getDeserializer(JsonNode.class);
DeserializationConfig cfg = mapper.deserializationConfig();
try (JsonParser parser = createParser(source)) {
JsonToken t = parser.currentToken();
if (t == null) {
t = parser.nextToken();
if (t == null) {
return cfg.getNodeFactory().missingNode();
}
}
DeserializationContext ctxt = mapper._deserializationContext(); // <-- not bound to `parser`
if (t == JsonToken.VALUE_NULL) {
return cfg.getNodeFactory().nullNode();
} else {
return deserializer.deserialize(parser, ctxt);
}
}
}
mapper._deserializationContext() returns a context whose per-read state (including _readCapabilities, normally derived from JsonParser.streamReadCapabilities()) has not been initialized, because the parser is never assigned to it. Any code path that consults a StreamReadCapability then dereferences null — _handleDuplicateProperty is the first place that happens, so the NPE only surfaces for input containing a duplicate property. A context created and bound to the parser through the normal read path would have _readCapabilities populated and would apply the existing duplicate-key handling.
Suggested fix
Bind the parser to the deserialization context before deserializing (so stream-read capabilities are populated), or read the tree through a parser-aware path (e.g. an ObjectReader/ObjectMapper.readTree(JsonParser) variant) rather than invoking deserializer.deserialize(parser, ctxt) with an unbound mapper._deserializationContext().
Impact
Any cached value whose JSON contains a duplicate property crashes on read instead of being handled. This is easy to hit in practice, for example:
- values written by an earlier Jackson 2 build (
GenericJackson2JsonRedisSerializer), which was more lenient about emitting duplicate keys; and
- polymorphic types where the
@JsonTypeInfo(include = As.PROPERTY, property = "…") type-id name coincides with a bean property of the same name (Jackson 2 emitted both as duplicate keys). Such entries can no longer be read back — even to be evicted — under the Jackson 3 serializer.
Summary
GenericJacksonJsonRedisSerializer(the Jackson 3 serializer, with default typing enabled) throws aNullPointerExceptionwhen deserializing any JSON object that contains a duplicate property name. The failure is inGenericJacksonJsonRedisSerializer.TypeResolver.readTree(byte[]), which builds aJsonNodetree using aDeserializationContextobtained frommapper._deserializationContext()— a context that is never bound to the parser, so its_readCapabilitiesisnull. As soon as the tree contains a duplicate key,BaseNodeDeserializer._handleDuplicatePropertycallsctxt.isEnabled(StreamReadCapability.DUPLICATE_PROPERTIES)and NPEs.Duplicate names are permitted by JSON (RFC 8259) and Jackson's own tree deserializer normally handles them gracefully (last value wins, unless
FAIL_ON_READING_DUP_TREE_KEYis set). Here they crash instead.Affected versions
tools.jackson).main(TypeResolver.readTreestill usesmapper._deserializationContext()directly).Minimal reproduction
No custom mapper, no special types — a default-typing serializer and a two-line JSON with a duplicate key:
Actual behaviour
Expected behaviour
The value should deserialize the same way Jackson's tree reader would (last value wins by default), or at least raise a meaningful
SerializationException— not aNullPointerExceptionfrom an uninitializedDeserializationContext.Root cause
TypeResolver.readTree(byte[]):mapper._deserializationContext()returns a context whose per-read state (including_readCapabilities, normally derived fromJsonParser.streamReadCapabilities()) has not been initialized, because the parser is never assigned to it. Any code path that consults aStreamReadCapabilitythen dereferencesnull—_handleDuplicatePropertyis the first place that happens, so the NPE only surfaces for input containing a duplicate property. A context created and bound to the parser through the normal read path would have_readCapabilitiespopulated and would apply the existing duplicate-key handling.Suggested fix
Bind the parser to the deserialization context before deserializing (so stream-read capabilities are populated), or read the tree through a parser-aware path (e.g. an
ObjectReader/ObjectMapper.readTree(JsonParser)variant) rather than invokingdeserializer.deserialize(parser, ctxt)with an unboundmapper._deserializationContext().Impact
Any cached value whose JSON contains a duplicate property crashes on read instead of being handled. This is easy to hit in practice, for example:
GenericJackson2JsonRedisSerializer), which was more lenient about emitting duplicate keys; and@JsonTypeInfo(include = As.PROPERTY, property = "…")type-id name coincides with a bean property of the same name (Jackson 2 emitted both as duplicate keys). Such entries can no longer be read back — even to be evicted — under the Jackson 3 serializer.