DEV Community

hepengju
hepengju

Posted on

Why Your Redis Values Look Like Garbage in Every GUI (and How to Fix It)

You connect your Redis GUI to the database your service writes to. You click a key. You get this:

\xac\xed\x00\x05sr\x00\x1dcom.example.order.OrderDetail\x00\x00\x00...
Enter fullscreen mode Exit fullscreen mode

Or, depending on the client, a wall of �.

This is not a bug in Redis and it is not a broken client. It is a serialization problem — and the real problem is not that nobody supports it. Most clients do. RedisInsight has shipped a Java serialized objects formatter since 2.10 (September 2022), Another Redis Desktop Manager has a JavaSerialize viewer, and both use the same library I do (java-object-serialization).

The problem is that the default is still UTF-8, you have to know the feature exists to switch, and what you get after switching differs a lot between clients.

I maintain RedisME, an open-source Redis GUI. This post covers four things: why the bytes look like that, why the usual workarounds hurt, why "supported" and "usable" are different claims, and what to do with formats nobody ships a decoder for.

Redis stores bytes. Types are your client's problem.

RESP is binary-safe. SET foo bar stores three bytes — 0x62 0x61 0x72. GET foo returns those three bytes. Redis has no idea what they mean.

So when a Java service writes an OrderDetail object, what actually happens is:

// Spring Data Redis' default, effectively
RedisSerializer<Object> serializer = new JdkSerializationRedisSerializer();
byte[] bytes = serializer.serialize(orderDetail);
Enter fullscreen mode Exit fullscreen mode

JdkSerializationRedisSerializer goes through ObjectOutputStream, which emits a stream starting with a fixed magic number:

Bytes Meaning
0xAC 0xED STREAM_MAGIC
0x00 0x05 STREAM_VERSION

That is your \xac\xed\x00\x05. It is a perfectly valid Java serialization stream, and it is an illegal UTF-8 byte sequence. By default, a GUI just decodes as UTF-8:

bytes -> decode as UTF-8 -> fail -> print � or \xNN -> done
Enter fullscreen mode Exit fullscreen mode

That is not laziness. Doing it properly needs format detection, a runtime that can deserialize, and handling for every case where deserialization fails. So the capability is usually built in but off by default — you have to know it exists and remember to switch.

Five serializers, five kinds of garbage

Java teams do not have one way to write objects to Redis. They have at least five, and each produces completely different bytes:

Serializer Where you see it What's stored GUI shows
JdkSerializationRedisSerializer Spring Data Redis default binary stream, starts AC ED 00 05 garbage
StringRedisSerializer manual toString() / JSON strings {"id":10086,...} readable
Jackson2JsonRedisSerializer explicit config JSON text readable
GenericJackson2JsonRedisSerializer JSON with @class {"@class":"com...","id":...} readable, with type noise
Redisson Codec (Kryo / FST / Marshalling / JSON) Redisson client custom binary header garbage
Protobuf / Hessian / in-house custom RPC stacks binary garbage

One table explains a lot of confusion. The moment a team migrates serializers, or runs Spring Data Redis and Redisson side by side, the same database holds multiple formats at once. And the default dropdown still says UTF-8.

Worse, many developers do not know which serializer their RedisTemplate uses, because someone else configured it three years ago and it was injected with @Autowired.

The three workarounds, and why they hurt

If clients support this, why do people still do it? Because it is off by default, because plenty of developers do not know the feature exists, and because not every client ships it — TinyRDM's built-in decoders cover Base64/GZip/Msgpack/PHP/Pickle but not Java, so you wire up an external command; RESP.app has no Java formatter at all.

1. redis-cli --raw

redis-cli -h 10.0.0.1 --raw get order:10086
Enter fullscreen mode Exit fullscreen mode

--raw only turns off redis-cli's output escaping. The bytes are unchanged. You get unprintable characters, and on some terminals you will need to reset afterwards.

2. A throwaway main method

public static void main(String[] args) throws Exception {
    byte[] raw = Files.readAllBytes(Paths.get("dump.bin"));
    Object o = new ObjectInputStream(new ByteArrayInputStream(raw)).readObject();
    System.out.println(JSON.toJSONString(o));
}
Enter fullscreen mode Exit fullscreen mode

This works. The workflow is: copy Base64 out of the GUI, save to a file, base64-decode, run Java, read output. Five steps to look at one key. And readObject needs the class on the classpath, so you will spend most of your time fighting ClassNotFoundException in a scratch project that does not have your domain classes.

3. DUMP + an online decoder

DUMP output includes an RDB version header and a CRC64 checksum — it is not the same as the raw GET bytes. And pasting production data into a third-party website is not a tradeoff worth discussing.

All three share the same flaw: they turn a two-second "let me look at this value" into a cross-tool, cross-process workflow. When you are debugging, you need to click through five keys and confirm they share a structure. Any approach that requires switching windows has already failed at that job.

Supported is not the same as usable

Let me be precise about what already exists. RedisInsight has had a Java formatter since 2.10, ARDM has a JavaSerialize viewer, and both are built on java-object-serialization — the same library RedisME uses. Nobody has a monopoly here.

So the interesting question is not "does it decode". It is what you get after it decodes, and how many steps stand between you and the answer. That is where implementations drift apart.

Layer 1 — built-in codecs

Next to UTF-8 / Base64 / Hex, RedisME ships three decoders in the Data Encoding dropdown:

  • JavaSerial — JDK serialization
  • Pickle — Python pickle, protocols 0–5
  • PhpSerial — PHP serialize() text

The library solves "can we read the bytes". What the value looks like afterwards is written by each client, and that layer is where the work is:

Case Raw deserialization output What I render
LocalDateTime / Instant / Duration nested {seconds, nanos} or epoch numbers readable timestamps
Java 16+ record fields fetched reflectively, often incomplete expanded JSON by field
ArrayList / HashMap / TreeSet toString()-ish [a, b, c] arrays and objects, collapsible
very large values the whole thing dumped into the page truncated preview, downloadable

There is also a structural difference: Hash / List / Set / ZSet support per-field codec selection. Open the field editor and pick a codec for one field; if the magic number matches, it preselects it. Some clients apply one formatter to the entire key value, which does not help when a hash holds mixed formats.

Pickle maps dict / list / set / bytes to JSON, and objects carrying $class / $type are viewable. PhpSerial keeps the class name as $class for O: instances and handles nesting (references via R: / r: are not supported).

All three are read-only — more on why below.

Layer 2 — auto-detection

Select Auto and the client walks the built-in decoders, matching on magic numbers rather than guessing. AC ED is unambiguously a Java stream; O: + trailing ; is almost certainly PHP serialize. That keeps false positives out of ordinary strings.

Layer 3 — custom codec scripts

This is where the real problems live: in-house binary protocols, compressed JSON, AES-encrypted values, your company's own serialization framework.

RedisME delegates the work to an external script over a minimal stdin/stdout contract:

# read: Redis raw bytes (base64) -> text shown in the editor
{your command} decode {wire_base64}

# write: editor text -> Redis raw bytes (base64)
{your command} encode {editor_text_utf8_base64}

# when base64 exceeds 8000 chars (Windows command-line limits)
{your command} decode --stdin
Enter fullscreen mode Exit fullscreen mode
Direction arg 1 arg 2 stdout
decode decode base64 of Redis raw bytes UTF-8 text
encode encode base64 of editor text (UTF-8) single line of base64
large payload same --stdin same

On failure, write to stderr and exit non-zero. The client surfaces the exact command it ran in the error dialog — which has saved me more times than I can count, because the cause is nearly always a space in a path or an interpreter missing from PATH.

The client ships templates for Python, Node, and Java. Export one, and you only edit two functions:

def decode(raw: bytes) -> str:
    """Redis raw bytes -> text for the editor"""
    return binascii.hexlify(raw).decode('ascii')   # replace with your logic

def encode(text: str) -> bytes:
    """editor text -> Redis raw bytes"""
    return binascii.unhexlify(text.strip())        # replace with your logic
Enter fullscreen mode Exit fullscreen mode

Your codec logic stays in your own repository. The GUI only shells out to it — which matters if you have compliance constraints, because it means you never hand private keys or an internal protocol to a third-party tool.

One trap: the bundled hex template "succeeds" on any input, because every byte sequence converts to hex. If you tick Auto detect on it, it will claim every value in the database and you will think the client is broken. Only auto-detect the codec you actually use, and make its decode throw when the format does not match.

Three design decisions worth explaining

1. Switching encodings never re-queries Redis.

STRING / Hash / List / Set / ZSet are transferred over IPC as base64 wire, always. The Data Encoding dropdown only changes presentation.

Why it matters: an HGETALL on a production hash can be tens of megabytes. Re-fetching on every dropdown change is not "switching views", it is DDoSing your own database. And the lag you would feel makes you stop exploring.

2. Large payloads go through stdin, not argv.

Windows caps a command line at 32767 characters. The protocol switches to --stdin at 8000 base64 characters — deliberately below the ceiling, because paths, arguments, and environment variables all consume the budget.

3. The built-in codecs are read-only on purpose.

Writing back means fully reconstructing a valid serialization stream: class version UIDs, field order, circular references. Deserialization is best-effort; serialization must be exactly right. A bad write leaves you with a key that no service can read — significantly worse than not being able to read it yourself.

For write-back, use a custom codec script with your local java / python / php. Your runtime, your correctness. I think that boundary should be explicit.

Limitations, stated plainly

  • No write-back through the three built-in decoders.
  • Custom codec scripts time out at 5 seconds by default — not the place for heavy inference.
  • Auto-detection only works on formats with a recognizable magic number.
  • Stream and JSON types have the encoding dropdown disabled; their handling differs.

If you look at a value once a month, redis-cli plus a scratch main method is fine. If your day is spent confirming what eight Java services wrote into the same key, this saves hours.

Try it

RedisME is a cross-platform Redis GUI built with Tauri (Rust + system WebView), GPL-3.0, under 10 MB, for Windows / macOS / Linux.

If you have hit this problem, I would genuinely like to know which serializer your team uses — I want to find the ones I have not covered yet.

Top comments (0)