Protobuf vs JSON
The same order is 42 bytes as protobuf and 137 bytes as compact JSON. Protobuf is smaller because it sends field numbers instead of names and writes numbers as varints instead of text; JSON can be read without a schema. Protobuf suits calls between services that share a .proto; JSON suits payloads that people, browsers or other companies read.
Updated
At a glance
| Protobuf | JSON | |
|---|---|---|
| On the wire | Binary: a field number and wire type, then the value | Text: each field's name, then its value |
| Schema | Required: a .proto file, compiled in or loaded at run time | Optional, such as JSON Schema or OpenAPI |
| Reading a payload | Needs the schema; without it a raw decode shows only field numbers | Any text editor |
| Renaming a field | Safe: the name is not on the wire, only the number | Breaks every reader of the old name |
| Adding a field | Old readers skip a field number they do not know | Most readers ignore a key they do not know |
| 64-bit integers | Exact | JavaScript loses precision past 2^53, so ProtoJSON writes them as strings |
| Binary data | Raw bytes | Base64 text, about a third larger |
Worked example: one order, both ways
The message:
syntax = "proto3";
package shop.v1;
message Order {
string id = 1;
int64 customer_id = 2;
repeated LineItem items = 3;
int32 total_cents = 4;
bool paid = 5;
}
message LineItem {
string sku = 1;
uint32 quantity = 2;
}As JSON, 137 bytes when written without spaces:
{
"id": "ord_1842",
"customerId": "90210",
"items": [
{
"sku": "MUG-01",
"quantity": 2
},
{
"sku": "TEE-L",
"quantity": 1
}
],
"totalCents": 4800,
"paid": true
}As protobuf, 42 bytes, 31% of the JSON:
| Field | Bytes (hex) | Size |
|---|---|---|
id | 0a 08 6f 72 64 5f 31 38 34 32 | 10 |
customer_id | 10 e2 c0 05 | 4 |
items (first) | 1a 0a 0a 06 4d 55 47 2d 30 31 10 02 | 12 |
items (second) | 1a 09 0a 05 54 45 45 2d 4c 10 01 | 11 |
total_cents | 20 c0 25 | 3 |
paid | 28 01 | 2 |
71 of the JSON's 137 bytes are field names and their quotes; protobuf spends one byte per field on a key that holds the field number and wire type. Numbers shrink too: 90210 takes 3 bytes as a varint, and true takes 1.
Paste CghvcmRfMTg0MhDiwAUaCgoGTVVHLTAxEAIaCQoFVEVFLUwQASDAJSgB into the protobuf decoder to read these bytes back without the .proto.
Converting protobuf to JSON
The official protobuf libraries include the JSON mapping, called ProtoJSON: protojson.Marshal in Go, JsonFormat.printer() in Java and json_format.MessageToJson in Python. It writes names in lowerCamelCase, 64-bit integers as strings, enums by name and bytes as base64, and it leaves out fields at their default value unless you ask for them.
Timestamps, durations and the other well-known types have JSON forms of their own; see well-known types in JSON. To get the JSON request for any message in a .proto, use the .proto to JSON tool.
Speed
This page measures size only. How fast each one encodes and parses depends on the language, the library and the message, so measure with your own messages before choosing on speed.
When JSON is the better choice
- A public API that other companies or browsers call without your
.proto. - Configuration, logs and payloads people read and edit by hand.
- Services that do not share a schema, or change shape faster than one can be agreed.
When protobuf is the better choice
- Calls between services you own, where every side can load the same
.proto. - Large or frequent messages, where the bytes on the wire add up.
- Schemas that evolve over years: field numbers let old and new versions read each other.
istek sends the JSON you write as protobuf and shows every reply as JSON, decoded with its schema. See istek.