Repository navigation
Recover root-level List.of()/Stream.toList() values on deserialization - #3446
seonwooj0810 wants to merge 2 commits into
Conversation
GenericJacksonJsonRedisSerializer's default typing intentionally omits a type hint for a root value whose concrete type is final and lives in the java.* namespace (see TypeResolverBuilder#useForType), since such types are typically private and cannot be constructed via reflection. Lists returned by List.of(...)/Stream.toList() fall into that category. On deserialization, however, requesting Object.class with default typing enabled always routes JSON arrays through Jackson's AsArrayTypeDeserializer, which expects the array's first element to be a type id. For a root array that was serialized without a hint, this misreads the first data element as an unresolvable type id and fails with a MismatchedInputException, even though the same value round-trips fine when wrapped in a plain ArrayList. Recover from that failure by decomposing the root JSON array and reading each element independently through the same ObjectMapper, which preserves any type hints nested one level down (only the outermost value is exempt from typing). To avoid masking a genuinely broken/renamed type id, the fallback only engages when the source cannot possibly be a valid [typeId, value] pair, i.e. it doesn't have exactly two elements led by a string. Closes spring-projects#2697 Signed-off-by: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com>
|
Measured on
Separately, |
|
Thanks for the careful re-check @renechoi — both hold up.
No push in this pass; flagging so it doesn't look ignored. |
Signed-off-by: seonwooj0810 <seonwooj0810@gmail.com>
|
@renechoi fixed in c07cf54. Recovery now reads each element from the original JSON tokens instead of converting tree nodes, preserving both hinted BigDecimal precision and ordinary Double values. The array guard runs before indexed access, and recovery failures are wrapped in SerializationException. Added regressions for the two-entry map, invalid nested type IDs, and mixed numeric values in nested lists across two round trips, covering both deserialize overloads. The map and precision tests both fail on bb552e2 with the reported NPE and 1.0 result.
|
Fixes #2697
What's wrong
GenericJacksonJsonRedisSerializer's default typing intentionally omits a type hint for a root value whose concrete type isfinaland lives in thejava.*namespace (TypeResolverBuilder#useForType), since such types are typically private and cannot be constructed via reflection. Lists returned byList.of(...)/Stream.toList()fall into that category (ImmutableCollections$ListN/List12etc.).On deserialization, requesting
Object.classwith default typing enabled always routes JSON arrays through Jackson'sAsArrayTypeDeserializer, which expects the array's first element to be a type id. For a root array that was serialized without a hint, this misreads the first data element as an unresolvable type id and fails with aMismatchedInputException/InvalidTypeIdException, even though the very same value round-trips fine once wrapped in a plainArrayList:This is a real-world footgun with
@Cacheable: a method returninglist.stream().toList()(or anyList.of(...)) silently breaks caching the moment the value round-trips through Redis, without any exception at write time - only at the next read.The fix
GenericJacksonJsonRedisSerializer#deserializenow catches theMismatchedInputExceptionthat surfaces from this scenario and, only when default typing is active and the caller asked forObject.class(the common@Cacheable/untyped-cache case), attempts a narrow recovery: decompose the root JSON array and re-read each element independently through the sameObjectMapper. Nested type hints are unaffected, since only the outermost value is ever exempt from typing, so e.g.List.of(List.of(1, 2))still recovers the innerListcorrectly.To avoid masking a genuinely broken/renamed type id elsewhere, the fallback only kicks in when the source cannot possibly be a valid
[typeId, value]pair - i.e. it doesn't have exactly two elements led by a JSON string. A two-element array led by a string remains ambiguous from the wire format alone (it could be a real type-wrapped value whose class was removed) and is intentionally left throwing, matching the concern a maintainer raised on the issue about not having "sufficient hooks to either always or never enable typing" for top-level arrays.Testing
Added regression tests in
GenericJacksonJsonRedisSerializerUnitTestscovering:Stream.toList()), plusList.of(...)and empty lists.SerializationException, so the fallback doesn't mask real errors.Verification done: ran the full
GenericJacksonJsonRedisSerializerUnitTests(44 tests) andRedisJsonSerializerSplitterUnitTests(85 tests, since it also exercises this serializer) via./mvnw test -Dtest=...- all passing, no regressions. Also confirmed no in-flight PR references this issue and no maintainer/CLA/DCO blocker (DCO sign-off included below).