Value Inspector Decodes Pickles, Java Objects and PHP Sessions From Redis and Valkey Without Running Them
Someone runs GET session:42 on a production Redis server to see why a user is stuck, and gets back "\xac\xed\x00\x05sr\x00\x11java.util.HashMap\x05\a\xda\xc1...". The data is all there. It’s in Java’s serialization format, behind redis-cli’s escapes, and nobody reads that by eye.
The Value Inspector does. Paste a value the way redis-cli printed it, or as hex, base64 or plain text, and it works out the format and decodes it: JSON, MessagePack, CBOR, BSON, Protocol Buffers, PHP sessions and igbinary, Java serialization, Python pickles and Ruby Marshal, inside base64, gzip, zlib, LZ4 or Snappy.

It runs in your browser and doesn’t send anything anywhere, and the page’s security policy stops it from fetching or loading anything from another site. Sessions and cached user records are exactly the kind of data that shouldn’t go to someone else’s server.
Every Framework Picks Its Own Format
A key-value store keeps bytes and never looks inside them, so the format is the application’s choice, and the defaults differ. Spring Data Redis and Spring Session use Java serialization. Django’s Redis cache pickles everything except plain integers, which it leaves as numbers so INCR can work on them. PHP sessions in Redis use PHP’s own session format, built on serialize(), and the phpredis extension can switch to igbinary. Ruby has Marshal. Teams that choose for themselves often go with JSON, gzipped once values get big, or MessagePack.
Most of these give themselves away in a few bytes. Java serialization starts with \xac\xed\x00\x05, a pickle with \x80 and its protocol number, Marshal with \x04\x08, igbinary with \x00\x00\x00\x02 and gzip with \x1f\x8b. The inspector checks those first, then text formats, then binary formats that have no signature. Each wrapper it removes becomes a layer, and it starts over on what’s inside, so base64 around gzip around JSON comes out as three steps, with the gzip checksum checked on the way.
Guessing Without a Signature
MessagePack, CBOR and Protocol Buffers have no signature, and that’s where a decoder can fool itself. Random bytes often parse as one of them: a stray byte becomes a small integer, another an empty map. So the inspector only accepts a reading that holds real text, floating-point numbers, booleans or nulls, and lists weaker readings as possibilities. Base64 and hex get the same care, since plenty of ordinary tokens happen to be valid base64, and a string is decoded only when what comes out makes sense.
To check, the inspector was fed 50,000 random base64 tokens and 20,000 random hex strings, the kind of IDs and hashes that fill a keyspace. It took none of them for anything but text. Of 20,000 random binary values, all but 21 came out as plain binary data, and those 21 were short values that happened to be valid text.
A Pickle That Would Run a Command
Pickles, Java serialization and Marshal can carry instructions along with the data. A pickle can tell Python to call any function while it loads, which is how a poisoned cache entry turns into code running on a server. The inspector never calls anything. It reads the opcodes and shows what they would build, so a pickle that would run os.system('echo hi') shows up as a call to posix.system with the argument "echo hi", and nothing more happens. That makes it a safe way to look at a value you don’t trust.
Checked Against the Libraries That Wrote the Values
Every test value was written by the real library and read back by the same language for comparison: Python 3.13 pickles in all six protocols, MessagePack, CBOR, BSON and Protocol Buffers, a Django 6.1 cache entry, PHP 8.3 serialize() output, sessions and igbinary, Ruby 3.3 Marshal, and Java 21 objects with inheritance, back-references and their own writeObject methods. All 53 matched value by value. Valkey 9.1.2 and Redis 8.10.2 printed stored values with --no-raw, and all 10 read back to the exact bytes stored. The gzip and zlib decoder, written from scratch so the page needs nothing else, matched Node’s zlib across compression levels and strategies.
The inspector is one JavaScript file with no dependencies, open source under the Apache License 2.0. From the command line it reads straight from redis-cli:
redis-cli --no-raw GET session:42 | node inspect/cli.js -
node inspect/cli.js value.bin --json > value.json
The manual, the tests and the values they replay are in the inspect folder on GitHub.