Skip to main content Skip to sidebar

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 TypeValueDescription
0Unsigned integerCompact variable-length encoding
1Negative integerEncoded as -1 minus the unsigned value
2Byte stringRaw binary data (no Base64 needed)
3Text stringUTF-8 encoded
4ArrayOrdered sequence of items
5MapKey-value pairs
6TagSemantic annotation (dates, bignum, etc.)
7Simple/floatBooleans, 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
FeatureJSONCBOR
EncodingText (UTF-8)Binary
Human-readableYesNo (hex dump required)
Binary dataBase64 (33% overhead)Native byte strings
Number typesSingle “number” typeInteger, float16/32/64, bignum
Schema languageJSON SchemaCDDL (RFC 8610)
StreamingLimitedBuilt-in indefinite-length items
Browser supportNativeRequires library
Typical sizeBaseline20-50% smaller
Parse speedBaseline2-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.

FeatureJSON LinesCBOR Sequences
DelimiterNewline (\n)None (self-delimiting)
EncodingTextBinary
Partial readsLine-by-lineItem-by-item
Binary dataBase64 per lineNative byte strings
Content typeapplication/jsonlapplication/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

References