JSON vs Protocol Buffers

The same records encoded three ways: JSON, MessagePack and Protocol Buffers. Every byte below comes from real encoders, so you can see where each format spends its space, how much gzip changes the picture, and what a protobuf reader does with data written under a different version of the schema.

JSON
1,598 B
MessagePack
1,213 B
Protobuf
741 B
Data
Records
Inspect
Bytes for 10 records
JSON 1,598
JSON, gzip n/a
MessagePack 1,213
Protobuf 741
Protobuf, gzip n/a

Gzip sizes are measured in your browser with its built-in compressor.

Protobuf bytes and what they mean
0a 39 08 a0 8d 06 12 0c 41 64 61 20 4c 6f 76 65 6c 61 63 65 1a 18 61 64 61 2e 6c 6f 76 65 6c 61 63 65 40 65 78 61 6d 70 6c 65 2e 63 6f 6d 2a 05 61 64 6d 69 6e 30 80 e2 cf aa 06 0a 47 08 b1 8d 06 12 0c 47 72 61 63 65 20 48 6f 70 70 65 72 1a 18 67 72 61 63 65 2e 68 6f 70 70 65 72 40 65 78 61 6d 70 6c 65 2e 63 6f 6d 20 01 2a 05 61 64 6d 69 6e 2a 07 62 69 6c 6c …
  • 2–5 id = 100000
  • 6–19 name = "Ada Lovelace"
  • 20–45 email = "[email protected]"
  • 46–52 roles = "admin"
  • 53–58 created_unix = 1700000000
  • 61–64 id = 100017
  • 65–78 name = "Grace Hopper"
  • 79–104 email = "[email protected]"
  • 105–106 active = true
  • 107–113 roles = "admin"

Each record is field 1 of a list message (tag 0x0a, then its length). Inside, each field is a tag, field number × 8 + wire type, then a varint, an 8-byte double or a length and bytes.

Per record
JSON
159.8 B
MessagePack
121.3 B
Protobuf
74.1 B
Compared with JSON
MessagePack
76%
Protobuf
46%
Changing the schema: a user record written with one version, read with another
Written with
Read with
Protobuf reader (id, name, email, active)
#FieldValue read
1id5000000123
2nameGrace Hopper
3email[email protected]
4activetrue

Unknown fields kept and passed through: 5, 6.

The same change in JSON
FieldValue read
id5000000123
nameGrace Hopper
email[email protected]
activetrue

JSON matches fields by name. Ignored by this reader: roles, createdUnix.

How to use the simulator

Pick a kind of data and a number of records. The bars show the encoded size in each format, and the inspector shows the bytes. The lower panel writes one user record with one version of the schema and reads it with another.

  • Compare sensor readings with log events. Numbers shrink a lot in protobuf; free text barely does.
  • Look at the gzip bars at 100 records: compression removes most of JSON's repeated field names, and the gap narrows.
  • Write with v2 and read with v1: the new fields come through as unknown fields. Read with v3 (renamed field) and nothing changes for protobuf, while the JSON reader loses the value.

The protobuf encoder produces the same bytes as Google's protobuf runtime, the MessagePack encoder the same bytes as the msgpack library, and decoding across schema versions gives the same values and unknown fields as the Python protobuf runtime.

JSON and Protocol Buffers

JSON (JavaScript Object Notation) is a text format for objects, arrays, strings, numbers, booleans and null. Every document describes itself: field names travel with the data, and any program can read it without knowing its structure in advance. Protocol Buffers (protobuf) is Google's binary format: the structure is declared in a .proto file, code is generated from it, and the encoded data carries only field numbers and values, so a reader needs the schema to make sense of it.

The choice affects size, speed, how readable the data is to people and tools, and how safely the format can change over time.

Size

Bytes for each dataset, raw and gzipped (gzip level 6, as computed when this page was built):

DataRecordsJSONJSON, gzipMessagePackProtobufProtobuf, gzip
User records11471331115974
User records101,5984481,213741427
User records10016,2081,78612,3817,5871,783
Sensor readings1239171265218223
Sensor readings102,3915152,6972,180933
Sensor readings10023,9722,33527,11521,8003,243
Log events11391311167480
Log events101,4653671,239820342
Log events10014,6231,13612,3658,1821,171

Protobuf saves the most on numeric data and on records with many short fields, where JSON's names, quotes and decimal digits dominate. On text-heavy data such as log messages the strings are the same bytes in every format and the difference shrinks. Gzip is good at removing JSON's repetition, so compressed JSON often comes close to protobuf; compressing protobuf gains less, because there is less repetition left. For a single small record gzip makes things bigger, since its header and trailer alone are 18 bytes.

How protobuf encodes data

A message is a sequence of fields. Each starts with a tag, the field number shifted left three bits plus a wire type: 0 for varints (integers, booleans, enums), 1 for 8-byte values (double), 2 for length-delimited data (strings, bytes, nested messages, packed lists) and 5 for 4-byte values (float). A varint stores 7 bits per byte, setting the high bit when more bytes follow, so 300 is 08 ac 02: the tag 0x08 for field 1, then 0xac 0x02. Negative int32 and int64 values take 10 bytes because they are sign-extended to 64 bits; the sint types use zigzag encoding to keep small negative numbers short.

proto3 leaves out fields equal to their default (0, false, empty string), packs repeated numbers into one length-prefixed block, and lets fields appear in any order. Decoding is a loop over tags; a field the reader does not know is skipped by its wire type, which is what makes adding fields safe.

Changing a schema safely

Services are upgraded one at a time, so for a while old code reads data written by new code and the other way round. The simulator's user record goes through four changes. Written with v2, which added two fields, and read back:

  • by v1, which lacks the new fields: they are skipped as unknown fields (3 of them, a repeated field counting once per value) and preserved if the message is written out again;
  • by v3, which renamed name to display_name: the value arrives as "Grace Hopper", since only the number 2 is on the wire;
  • by v4, which narrowed id to int32: 5,000,000,123 is truncated to 705032827, silently;
  • by v5, which deleted email and reused number 3 for an int64: the string arrives with the wrong wire type and is set aside as unknown, so login_count reads as 0. Had both been integers it would have been misread without any sign.

The rules that follow, from the protobuf language guide: adding fields is safe; never change a field's number; never reuse a deleted field's number, and list it as reserved so nobody can; int32, uint32, int64, uint64 and bool are compatible but values are truncated; string and bytes are compatible when the bytes are valid UTF-8. Renaming is safe for the binary format but not for protobuf's JSON mapping or for code that uses the old name. JSON has the opposite profile: names are the contract, so renames break readers, but type and number mix-ups cannot happen silently in the same way.

Other trade-offs

  • Speed. Parsing decimal text, escaping strings and building generic maps costs more than reading varints into generated structs, so protobuf usually encodes and decodes faster; how much depends heavily on the library and language, and modern JSON parsers are fast.
  • Readability. JSON can be read in a terminal, logged, diffed and edited by hand. Protobuf needs the schema and a tool (protoc --decode, or --decode_raw for numbers only).
  • Types. JSON has one number type, and JavaScript reads it as a double, so integers above 253 lose precision; protobuf's JSON mapping writes int64 as a string for that reason. Protobuf has fixed integer widths, enums and a bytes type.
  • Tooling. Protobuf gives typed, generated clients in many languages and a single definition of the API; JSON needs a separate schema (JSON Schema, OpenAPI) to get the same guarantees.

Other formats

MessagePack and CBOR (RFC 8949) are binary encodings of the JSON data model: no schema, compact numbers, but field names in every record. Apache Avro writes values with no field tags at all and relies on the writer's schema being available to the reader, which makes it very compact for files of many records. Apache Thrift is close to protobuf in design. FlatBuffers and Cap'n Proto lay data out so it can be read in place without a decoding step, trading some size for zero-copy access.

History

Douglas Crockford specified JSON in the early 2000s as a subset of JavaScript literals; it was published as RFC 4627 in 2006, as ECMA-404 in 2013, and as the current standard RFC 8259 in 2017. Protocol Buffers began at Google in 2001 and were open-sourced in 2008 as proto2; proto3, which simplified the language and dropped required fields, arrived with protobuf 3.0 in 2016. MessagePack was created by Sadayuki Furuhashi.

JSON vs protobuf questions and answers

How much smaller is protobuf than JSON?

It depends on the data. For 100 user records here protobuf is 47% of the JSON size, for 100 series of sensor readings 91%, and for 100 log events, which are mostly free text, 56%. Gzip shrinks JSON far more than protobuf, because it removes the repeated field names: gzipped, the user records are 1,786 bytes as JSON and 1,783 as protobuf.

Why is protobuf smaller than JSON?

A protobuf field is identified by a small number instead of its name, integers are written as variable-length binary (300 takes 2 bytes, not 3 characters plus quotes and separators), floating-point numbers take a fixed 8 bytes rather than up to 17 digits, fields with default values are left out, and repeated numbers are packed into one length-prefixed block.

Can I add a field to a protobuf message without breaking clients?

Yes. Old readers skip the new field number, keeping it as an unknown field, and new readers give it its default value when it is missing. Never change an existing field’s number or reuse the number of a deleted field; mark deleted numbers as reserved.

Is renaming a protobuf field a breaking change?

Not for the binary format, because names never go over the wire: a reader with the new name decodes the old bytes correctly. It does break anything that uses names, such as protobuf’s JSON mapping, text format and generated code that refers to the old name.

What happens if I change a field from int64 to int32?

int32, uint32, int64, uint64 and bool are wire-compatible, so the reader decodes the value but truncates it to 32 bits like a C cast. The id 5,000,000,123 in this page’s example is read as 705032827 by a reader with int32.

Is MessagePack a good middle ground?

MessagePack is binary JSON: same data model, no schema, smaller numbers and no quotes or commas, but every record still carries its field names. Here it lands between the two, 12,381 bytes for 100 users against 16,208 for JSON and 7,587 for protobuf.

Further reading

Published Updated