JSON vs CBOR
JSON is the default choice for data interchange on the web. It is human-readable, universally supported, and simple to debug. But when payload size, parsing speed, or binary data handling matter, CBOR (Concise Binary Object Representation) offers a compelling alternative.
This post compares the two formats, explains when CBOR is worth the trade-off, and includes Go benchmarks to quantify the difference.
What is JSON
JSON (JavaScript Object Notation, RFC 8259) is a text-based format that represents data as key-value pairs, arrays, strings, numbers, booleans, and null. Every value is encoded as UTF-8 text.
{
"name": "sensor-01",
"temperature": 23.5,
"active": true,
"readings": [23.1, 23.3, 23.5]
}
Strengths: human-readable, universal tooling, easy debugging, native in browsers.
Weaknesses: verbose encoding, no binary data support (requires Base64), ambiguous number precision, slower parsing compared to binary formats.
What is CBOR
CBOR (Concise Binary Object Representation, RFC 8949) is a binary format modeled after JSON’s data model. It supports the same types (maps, arrays, strings, numbers, booleans, null) plus native binary byte strings, tags for dates/timestamps, and arbitrary-precision numbers.
CBOR encodes values using a type-length-value scheme. Each item starts with a major type (3 bits) and additional info (5 bits), followed by optional length and payload bytes:
block-beta
columns 4
block:header:2
majorType["Major Type (3 bits)"]
addInfo["Additional Info (5 bits)"]
end
length["Length (0-8 bytes)"]
payload["Payload (N bytes)"]
style majorType fill:#d4edda,stroke:#28a745,stroke-width:1px
style addInfo fill:#e1f5ff,stroke:#0366d6,stroke-width:1px
style length fill:#fff3cd,stroke:#ffc107,stroke-width:1px
style payload fill:#f8d7da,stroke:#dc3545,stroke-width:1px
The major types cover all common data structures:
| Major Type | Value | Description |
|---|---|---|
| 0 | Unsigned integer | Compact variable-length encoding |
| 1 | Negative integer | Encoded as -1 minus the unsigned value |
| 2 | Byte string | Raw binary data (no Base64 needed) |
| 3 | Text string | UTF-8 encoded |
| 4 | Array | Ordered sequence of items |
| 5 | Map | Key-value pairs |
| 6 | Tag | Semantic annotation (dates, bignum, etc.) |
| 7 | Simple/float | Booleans, null, float16/32/64 |
Format Comparison
flowchart TB
subgraph json["JSON"]
J1["Text-based (UTF-8)"]
J2["No binary support"]
J3["Number as text"]
J4["Human-readable"]
J5["Schema-less"]
end
subgraph cbor["CBOR"]
C1["Binary encoding"]
C2["Native byte strings"]
C3["Typed numbers (int/float/bignum)"]
C4["Machine-readable"]
C5["Schema-less + CDDL optional"]
end
subgraph tradeoff["Choose based on"]
T1["Debuggability vs size"]
T2["Tooling maturity vs performance"]
T3["Browser support vs IoT constraints"]
end
json --> tradeoff
cbor --> tradeoff
style J1 fill:#e1f5ff,stroke:#0366d6,stroke-width:1px
style J2 fill:#f8d7da,stroke:#dc3545,stroke-width:1px
style J3 fill:#fff3cd,stroke:#ffc107,stroke-width:1px
style J4 fill:#d4edda,stroke:#28a745,stroke-width:1px
style J5 fill:#e1f5ff,stroke:#0366d6,stroke-width:1px
style C1 fill:#e1f5ff,stroke:#0366d6,stroke-width:1px
style C2 fill:#d4edda,stroke:#28a745,stroke-width:1px
style C3 fill:#d4edda,stroke:#28a745,stroke-width:1px
style C4 fill:#e1f5ff,stroke:#0366d6,stroke-width:1px
style C5 fill:#e1f5ff,stroke:#0366d6,stroke-width:1px
| Feature | JSON | CBOR |
|---|---|---|
| Encoding | Text (UTF-8) | Binary |
| Human-readable | Yes | No (hex dump required) |
| Binary data | Base64 (33% overhead) | Native byte strings |
| Number types | Single “number” type | Integer, float16/32/64, bignum |
| Schema language | JSON Schema | CDDL (RFC 8610) |
| Streaming | Limited | Built-in indefinite-length items |
| Browser support | Native | Requires library |
| Typical size | Baseline | 20-50% smaller |
| Parse speed | Baseline | 2-5x faster |
Encoding Example
Consider encoding the same data in both formats:
type SensorData struct {
Name string `json:"name" cbor:"name"`
Temperature float64 `json:"temperature" cbor:"temperature"`
Active bool `json:"active" cbor:"active"`
Readings []float64 `json:"readings" cbor:"readings"`
}
JSON output (67 bytes, human-readable):
{"name":"sensor-01","temperature":23.5,"active":true,"readings":[23.1,23.3,23.5]}
CBOR output (57 bytes, hex representation):
a4 646e616d65 6973656e736f722d3031
6b74656d7065726174757265 fb4037800000000000
66616374697665 f5
68726561644696e6773 83 fb40371999... fb403749999... fb4037800000...
The CBOR encoding is more compact because:
- Keys and strings store length as integer bytes rather than surrounding quotes
- Numbers use native binary representation (8 bytes for float64) instead of ASCII digits
- Booleans encode as a single byte (
f5= true) instead of 4-5 characters - No separators (commas, colons) are needed between items
CBOR Sequences
Standard CBOR encoding produces a single self-contained item. RFC 8742 defines CBOR Sequences: a stream of concatenated CBOR data items without a wrapping array or any framing. Each item is independently decodable, so a decoder can process them one at a time as they arrive.
This is useful for append-only logs, streaming telemetry, and pipe-based protocols where producers write items incrementally and consumers read them without waiting for the entire payload.
flowchart LR
subgraph sequence["CBOR Sequence (stream)"]
direction LR
I1["CBOR Item 1"]
I2["CBOR Item 2"]
I3["CBOR Item 3"]
I4["..."]
end
Producer -->|"encode & append"| I1
I1 --- I2
I2 --- I3
I3 --- I4
I4 -->|"decode one at a time"| Consumer
style I1 fill:#d4edda,stroke:#28a745,stroke-width:1px
style I2 fill:#e1f5ff,stroke:#0366d6,stroke-width:1px
style I3 fill:#fff3cd,stroke:#ffc107,stroke-width:1px
style I4 fill:#f8d7da,stroke:#dc3545,stroke-width:1px
JSON Equivalent: JSON Lines
JSON has a similar pattern called JSON Lines (NDJSON): one JSON object per line, separated by newlines. CBOR Sequences achieve the same goal but without delimiter characters - CBOR items are self-delimiting because each item’s length is encoded in its header.
| Feature | JSON Lines | CBOR Sequences |
|---|---|---|
| Delimiter | Newline (\n) | None (self-delimiting) |
| Encoding | Text | Binary |
| Partial reads | Line-by-line | Item-by-item |
| Binary data | Base64 per line | Native byte strings |
| Content type | application/jsonl | application/cbor-seq |
When to Use CBOR
CBOR is a strong choice when:
- IoT and embedded devices - constrained bandwidth and processing power benefit from compact binary encoding
- Binary data - images, certificates, or firmware payloads embed directly without Base64 overhead
- High-throughput APIs - microservices exchanging large volumes of structured data gain measurable latency reduction
- COSE and WebAuthn - these standards mandate CBOR; you cannot avoid it in those contexts
- Storage - smaller serialized size reduces disk and cache usage
When to Keep JSON
JSON remains the better choice when:
- Browser clients - native
JSON.parse()is highly optimized and requires no additional dependencies - Debugging and logging - human-readable output simplifies troubleshooting
- Public APIs - most API consumers expect JSON; CBOR adds friction for integrators
- Configuration files - readability matters more than compactness
- Small payloads - the size and speed difference is negligible for payloads under a few hundred bytes