Protobuf Text Format vs Binary Wire Format — Why One Converts to JSON and the Other Can't

By · Published

Protocol Buffers has two separate serializations, and mixing them up is the most common confusion around the format. The binary wire format is what goes over the network. It's compact and fast, and it identifies fields by number, not name. You need the compiled .proto schema to read it, because the bytes alone don't say whether field 3 is called user_id or account.

Text format is the human-readable one. It's self-describing, with field names written out as text, and it shows up in config files and debug logs. That's the one this tool converts. A browser tool with no schema input has no way to interpret field numbers, but field names in a pasted document need no schema at all.

Repeated fields are the same key written more than once

Text format has no array syntax at the field level. A repeated field is just the same key written several times. The parser stores a key as a plain scalar the first time it sees it, and converts it to an array only when the same key shows up a second time:

tags: "urgent"
tags: "billing"
tags: "customer-a"

↓

{ "tags": ["urgent", "billing", "customer-a"] }

A field that appears once stays a plain scalar in the JSON output. It doesn't become a one-element array. If your code expects tags to always be an array, even with a single tag, normalize it yourself after conversion. The parser reports what appeared in the text. It can't know the field's declared cardinality, because that lives in the schema.

Advertisement

Escape sequences: quotes work, backslash-n doesn't

Know this before you paste a text-proto value copied from a debug log. Inside quoted strings, the parser treats a backslash as "drop the backslash, keep the next character as-is." That handles an escaped quote (\") correctly. It does not interpret the C-style escapes that protobuf text format supports. \n loses its backslash and comes through as a plain letter n, not a newline:

message: "line one\nline two"

↓ (what you might expect)
{ "message": "line one\nline two" }   // with an actual newline

↓ (what this parser actually produces)
{ "message": "line onenline two" }   // backslash dropped, "n" kept literally

So if a value comes out with a stray letter where a newline or tab should be, that's the cause. Values made of ordinary characters and escaped quotes convert as expected. The ones that don't decode are \n, \t, and similar control-character escapes.

Numbers, booleans, and bare identifiers are guessed from the text

An unquoted value is checked against true/false first, then run through Number(). enabled: true becomes a JSON boolean, retries: 3 becomes a JSON number, and anything that parses as neither stays a string. Nothing tells the parser that a field is an enum whose value happens to look like a word. It goes by what the text looks like, since there's no schema to consult.

Advertisement

Try the Protobuf Text ↔ JSON →