Protobuf Wire Decoder
Decode protobuf bytes with no .proto — field numbers, wire types and ranked value guesses, entirely on this device.
Without a .proto file the field names do not exist in these bytes. Every value below is a guess, ranked by likelihood.
Click the badge to pin the encoding.
Decoded fields appear here.
Selected field
Select a field to see how its bytes can be read.
Press ? for shortcuts
Do it offline:$ protoc --decode_raw < payload.bin
How to use it
- Paste the bytes into the input pane, or drop a .bin capture on it. Hex and Base64 are both read; the badge shows which one was detected, and you can pin it yourself when a payload happens to be legal as either.
- Read the tree. Each row prints the field number, the wire type, the byte range it occupies and the leading reading of its bytes; child rows appear under any length-delimited slice that decoded cleanly as a nested message.
- Open a row to see every ranked reading of the same bytes, each carrying a written speculative badge and a likelihood word. Switch to the hex dump with d when you want the raw bytes beside the offsets instead.
- Copy a field's bytes, copy or download the whole tree, or share the state as a link. Payloads that look credential-shaped are never kept between visits, and the page says so when it skips the save.
Paste protobuf wire bytes as hex or Base64, or drop a .bin file, and the tree appears as you type: field numbers, wire types, byte ranges and ranked readings for every value. Without a .proto file the field names do not exist in these bytes. Every value below is a guess, ranked by likelihood. Nothing is uploaded — the decode runs on this device.
How it works
Every field starts with a tag: one varint holding the field number shifted left three places, with the wire type in the bottom three bits. Byte 0x08 is field 1, wire type 0; byte 0x12 is field 2, wire type 2. That is the whole addressing scheme — six wire types (0 varint, 1 fixed 64-bit, 2 length-delimited, 3 and 4 group start and end, 5 fixed 32-bit) tell a reader how many bytes to consume next, and nothing else about the field travels on the wire.
Varints are base-128 groups written least-significant first, each byte spending its top bit as a continuation flag. 150 becomes 0x96 0x01: seven low bits, continue, then one more group. Ten groups carry 64 bits, which is why the readings here are computed with BigInt and never with a JavaScript number — a uint64 above 2^53 would silently lose its last digits otherwise, and a wrong digit in a decoded identifier is worse than no decode at all.
Wire type 2 is where certainty ends. The length prefix says how many bytes belong to the field and nothing at all about what they are, so the same slice can be a string, a nested message, a packed repeated list or opaque bytes — and often it is valid as several at once. This page decodes each reading that actually works, orders them by a fixed documented rule, and labels every one of them a guess. Only the .proto that produced the bytes can settle it.
Examples
089601# Decoded without a .proto: field names are unknown and every value below is a guess.
1: varint [1,3) uint64 150The specification's first example. Tag 0x08 is field 1 at wire type 0, and the two varint groups 0x96 0x01 carry 150 — held at bytes 1 to 3 of the input.
120774657374696e67# Decoded without a .proto: field names are unknown and every value below is a guess.
2: len=7 [2,9) string "testing"Tag 0x12 is field 2 at wire type 2, the next byte declares seven bytes, and those seven are well-formed printable UTF-8 — so the string reading leads. The packed and raw-bytes readings are still listed under the row.
GgMIlgE=# Decoded without a .proto: field names are unknown and every value below is a guess.
3: len=3 [2,5) message 1 field
1: varint [3,5) uint64 150The embedded-message vector, pasted as Base64 instead of hex. Field 3's three bytes decode as a complete message holding the first example again, so the child row is indented under it — a guess the notes declare rather than assert.
089680Stopped at byte 1: a varint value is cut off by the end of the input — its last byte sets the continuation bit but no byte follows.A capture cut one byte short. The last group still sets its continuation bit, so no honest value exists; the message names the byte where reading stopped and the pane keeps your input untouched.
0a0438303830# Decoded without a .proto: field names are unknown and every value below is a guess.
1: len=4 [2,6) string "8080"
7: varint [3,4) uint64 48
7: varint [5,6) uint64 48Four bytes that are genuinely two things at once: the text "8080", and a message of two field-7 varints, because 0x38 is also a legal tag. Short printable ASCII prints first by the documented tie-break, and both readings stay on the row.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
An empty slice has three honest readings
A length-delimited field declaring zero bytes could be an empty string, an empty message or an empty packed list — the wire cannot tell them apart because all three encode to the same two bytes. The row says so instead of picking one.
Hex and Base64 can be the same characters
A payload made only of digits and the letters a to f is legal in both alphabets, and the two readings give different bytes. The badge then reads ambiguous, a chip records that hex was tried first, and clicking the badge steps the pin through Auto, Hex and Base64 so you can settle on the one you meant.
Groups are deprecated, not gone
Wire types 3 and 4 open and close a group, the pre-2015 way of nesting a message. proto3 removed them and proto2 discourages them, yet stored payloads still carry them, so they decode here as a subtree with the deprecation noted.
Ten bytes for minus one is normal
A negative int32 or int64 is sign-extended to 64 bits before encoding, so -1 costs ten varint bytes every time. That is exactly why sint32 and sint64 exist: zigzag maps small negatives back onto small varints. The note names the case when it appears.
The same field number twice is not a mistake
Repetition is how the format writes a list, and for message-typed fields a later copy merges into the earlier one rather than replacing it. Two rows with one number are therefore expected; the note points it out because it surprises people reading a dump.
Common errors
Stopped at byte 1: the varint length prefix is cut off by the end of the input — the tag declares one but no length byte follows.- The tag arrived but the length that follows it did not. Usually a copy that clipped the tail, or a hex string pasted one byte at a time. Re-copy from the source and the row completes.
Stopped at byte 37: the varint length prefix runs past the end of the input — 12 bytes were declared, 4 remain.- The field promised more bytes than the paste contains, so both numbers are printed: what was declared, and what is actually left. This is the signature of a truncated download or a log line that wrapped and lost its end.
Stopped at byte 0: field number 0 is not a legal protobuf field number — field numbers start at 1.- Almost always a gRPC body: those frames begin with a compression flag byte and a four-byte length, and byte 0 is a zero the wire format cannot read as a tag. Drop the first five bytes and the rest decodes.
Stopped at byte 0: wire type 7 is not defined — protobuf uses wire types 0, 1, 2, 3, 4 and 5.- Two of the eight tag codes were never assigned, so hitting one means these bytes are not protobuf at all — plain text, a JSON body or a compressed blob read as a tag will land here quickly.
Stopped at byte 1: a varint runs past 10 bytes — protobuf varints encode at most 64 bits.- Ten bytes carry sixty-four bits, and an eleventh has nowhere to put its payload, so the reader stops rather than wrap silently. In practice this means the byte stream drifted: one inserted or deleted byte turns every tag after it into noise.
FAQ
Can this tool show the field names?
No, and nothing can from these bytes alone. The wire format stores a field number and a wire type per field; names, declared types, units and enum labels live only in the .proto file that generated the code. This page never invents a placeholder name — rows are addressed by number and by path, such as 3.1, so what you read back is exactly what the bytes carry.
How does it decide a slice is a nested message?
It re-runs the decoder over the slice and accepts the result only when the reading consumes every byte, produces at least one field, keeps each field number inside 1 to 536870911, uses no undefined wire type and stays inside the depth budget. Short ASCII text passes that test surprisingly often, which is why a message reading is always labelled a guess and never the only reading offered.
What do the wire type numbers mean?
They tell a reader how many bytes to take next, nothing more. Type 0 is a varint, type 1 is eight fixed bytes, type 2 is a varint length followed by that many bytes, types 3 and 4 open and close a deprecated group, and type 5 is four fixed bytes. Types 6 and 7 were never defined, so meeting one is proof the input is not protobuf.
Why is one varint shown as several different numbers?
Because one varint really is several numbers until a schema says otherwise. The same groups read as uint64, as a two's-complement int64, as a zigzag sint64, as a bool when the value is 0 or 1, and as an int32 or enum ordinal when it fits. Each row prints its own value with a likelihood word so you can match it against what your service claims to send.
Can I paste a gRPC response body?
Only after removing the framing. A gRPC message on the wire is prefixed by one compressed-flag byte and a four-byte big-endian length, and that leading zero byte stops the decode immediately with a field-number-zero error. Strip those five bytes per message and the remainder decodes normally; unwrapping the frames automatically is not built here today.
How big a payload will it take, and where does it go?
Up to four megabytes — the refusal names the same 4 MB figure — refused with a typed message past that rather than truncated, and the input pane keeps whatever you pasted. The bytes go nowhere: decoding, the hex dump and every ranked reading are computed in this page, dropped files are read locally, and credential-shaped payloads are not written to storage between visits.
Keyboard shortcuts
- ⌘/Ctrl + Enter
- Re-decode the pasted bytes right now.
- ⌘/Ctrl + S
- Save the decoded tree as a .txt file, not the web page.
- ⌘/Ctrl + C
- Copy the tree when no text is selected.
- ⌘/Ctrl + Shift + C
- Copy the tree even over a live selection.
- e
- Cycle the encoding: auto, hex, Base64 (outside text fields).
- d
- Switch between the tree and the hex dump.
- ? or ⌘/Ctrl + /
- Show the shortcut sheet for the decoder.
- Esc
- Close the shortcut sheet and leave the bytes alone.