-
[OPIK-6370] [BE] fix: tolerate plain-string UUIDs in Redis stream deserialization (#6610)
发布于
2026-05-07 15:52:35 +00:00 - [NA] [BE] fix: tolerate plain-string UUIDs in Redis stream deserialization
The Redis stream codec used by experiment_denormalization_stream and other
Java-codec streams wraps a Jackson ObjectMapper with default typing enabled.
Older opik-backend versions ran on a different default-typing configuration
and left messages with UUID fields serialized as plain strings in PEL; the
current version's deserializer expects Jackson's As.WRAPPER_ARRAY shape
(["java.util.UUID",""]). XAUTOCLAIM repeatedly tries to reclaim those
stuck messages, fails with MismatchedInputException, and emits an ERROR log
every pending-message-duration window.Add a LenientUUIDDeserializer that accepts both shapes and register it on
the mapper handed to JsonJacksonCodec inside RedisStreamCodec. Scoped to
the Redis stream codec only — the global JsonUtils.MAPPER is untouched, so
HTTP API JSON behavior is unchanged.Includes a unit test covering plain-string, As.WRAPPER_ARRAY, and a
round-trip case to confirm we don't break the happy path.- [NA] [BE] fix: override deserializeWithType so the lenient UUID deserializer fires under WRAPPER_ARRAY default typing
The original commit registered LenientUUIDDeserializer via addDeserializer
on the codec mapper, but that only wires it as the value-level deserializer
called AFTER the TypeDeserializer strips the wrapper. With Redisson's
JsonJacksonCodec enabling default typing, every UUID property gets an
AsArrayTypeDeserializer in front of it: it expects START_ARRAY for the
type id and explicitly throws MismatchedInputException on a bare string
before delegating to the value deserializer. So plain-string UUIDs in PEL
were never reaching our lenient logic.Fix by overriding deserializeWithType: it runs BEFORE the type deserializer
takes over. We peek at the current token and short-circuit when it's a
VALUE_STRING (legacy bare-string format), letting the standard UUIDDeserializer
parse it directly. For START_ARRAY we delegate to the type deserializer's
normal path, so current WRAPPER_ARRAY-format messages still decode correctly.Also adds a production-shape test that wires LenientUUIDDeserializer onto a
real JsonJacksonCodec (default typing on, snake_case naming, etc.) and
verifies all three round-trips: encoded shape contains the wrapper as
expected, plain-string payloads decode, WRAPPER_ARRAY payloads decode.
This is the test that caught the gap left by the original commit.- [NA] [BE] fix: assert END_ARRAY in WRAPPER_ARRAY UUID branch to reject malformed payloads
The wrapper-array branch advanced past the type-id and value tokens but
didn't assert the next token was END_ARRAY, so a payload like
["...","","extra"] would silently parse the UUID and leave the
parser positioned mid-array — corrupting the next field. Throw a
MismatchedInputException when the closing token isn't END_ARRAY.Adds a regression test that builds a 3-element wrapper-array payload and
asserts the deserializer rejects it instead of silently accepting it.下载附件