Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

Bytes, text and JSON

~45 min

A network moves bytes and nothing else. The temperature your sensor measured, the name of the greenhouse bench, the command to open a vent: each one crosses the wire as a row of numbers from 0 to 255. Whoever receives them has to know how to turn those numbers back into meaning. This section covers the three ideas that make that possible: how text becomes bytes, how to look at bytes directly, and JSON, the text format most device messages use.

Bytes and text

A byte is a small number, anything from 0 to 255. Every file, message and picture a computer handles is, underneath, a long row of bytes.

Text is letters, digits and symbols, the things people read. To send text, a computer needs an agreed rule that turns each character into bytes and back again. That rule is called an encoding. A byte on its own means nothing. The byte 65 is the letter A only because both sides agreed to read it as text.

The oldest common rule is ASCII. It covers English letters, the digits, punctuation and a space, and gives each one a single byte. Capital A is 65. The digit 0 is 48.

ASCII has no room for é, the degree sign, Chinese or emoji. UTF-8 is the rule that covers all of them. It uses one byte for every ASCII character, exactly as ASCII does. Every other character takes two, three or four bytes. The degree sign takes two. A seedling emoji takes four.

So here is the trap. Counting the characters you see does not tell you how many bytes there are. "21.5 °C" looks like 7 characters, but it is 8 bytes. Memory and message limits are counted in bytes, so a program must count bytes.

This section builds three ideas: bytes, text, and JSON, a text format for structured data that the last part of the section teaches. A byte is 8 bits, so it holds one of 256 values, 0 to 255, written in hex as 0x00 to 0xFF. Text is a sequence of characters. An encoding is the rule that turns each character into bytes and back. Bytes have no meaning of their own: 0x41 is the letter A under one encoding, the number 65 to a program reading an unsigned byte, and part of a larger number to a program reading two bytes at once.

ASCII is the oldest encoding still in daily use. It assigns 128 characters, English letters, digits, punctuation and a few control codes, to the values 0–127, one byte each. A is 0x41, a is 0x61, the digit 0 is 0x30, a space is 0x20, { is 0x7B.

UTF-8 encodes every character in Unicode, the catalogue of more than 150 000 characters from every writing system plus symbols and emoji. It uses 1 to 4 bytes per character, and its first 128 one-byte codes are exactly ASCII, so plain ASCII text is already valid UTF-8. Other characters take more:

Character Unicode code point UTF-8 bytes Count
C U+0043 43 1
° (degree sign) U+00B0 C2 B0 2
é U+00E9 C3 A9 2
€ U+20AC E2 82 AC 3
🌱 (seedling) U+1F331 F0 9F 8C B1 4

So a character count is not a byte count. 21.5 °C is 7 characters and 8 bytes. Buffers, payload limits and radio packets are sized in bytes, so firmware must count the encoded bytes, not the characters a person sees. UTF-8 is the encoding of the web, of JSON, and of MQTT's topic names; assume it unless a document says otherwise.

Characters versus bytes

Pick each sample. Before you look at the counts, predict how many bytes it takes in UTF-8. Then type the name of your own greenhouse bench and check.

Loading interactive…

Worked example

Sizing a buffer for a label

  1. 1

    A device stores its display label in a 16-byte buffer, with one byte kept for the end-of-string marker C++ needs. Will Gewächshaus 2 (German for "greenhouse 2") fit?

  2. 2

    Count characters. G-e-w-ä-c-h-s-h-a-u-s, a space, 2: 13 characters.

  3. 3

    Count bytes. Every character is ASCII except ä, which is 2 bytes in UTF-8. So 12 \times 1 + 2 = 14 bytes.

  4. 4

    Add the terminator. 14 + 1 = 15 \le 16. It fits, with one byte to spare. A label with two more non-ASCII letters, or one emoji, would not, even though it might still look short.

Hex dumps

Sometimes you need to see the actual bytes a program sent, not the text it was meant to be. A hex dump shows them.

It prints the bytes in rows of 16. Each byte appears as two hex digits. Hex is counting in sixteens, using the digits 0 to 9 and then the letters a to f. So 7b is 7 sixteens plus 11, which is 123.

Each row starts with a position number, also in hex, called the offset. It says how far into the data that row begins, counting from 0. The second row starts at 00000010, which is 16 in ordinary numbers. To find any byte's position, add its row's offset to its place along the row.

On the right, the same bytes are shown as characters, where that makes sense. Anything that isn't a plain letter, digit or symbol shows as a dot. That's how you spot a hidden line break at the end of a message, or a degree sign that arrived broken.

A hex dump shows raw bytes as text, one row per 16 bytes. Each row starts with an offset, the position of its first byte counted from 0 and written in hex, then the 16 bytes as two hex digits each, then the same bytes as characters, with a dot for any byte that isn't printable ASCII. Here are the 18 bytes of the JSON text {"t":21.5,"rh":64}, as hexdump -C or xxd-style tools print them:

00000000  7b 22 74 22 3a 32 31 2e  35 2c 22 72 68 22 3a 36  |{"t":21.5,"rh":6|
00000010  34 7d                                             |4}|

The second row's offset is 0x10, which is 16: it starts with the seventeenth byte. A byte's offset is its row's offset plus its column, so 7d (the closing brace) is at \texttt{0x10} + 1 = \texttt{0x11}, or 17. Reading a hex dump answers questions a printed string hides: whether a payload ends with an invisible newline (0a), whether a degree sign arrived as the two bytes c2 b0 or got mangled, whether a number was sent as text digits (32 31) or as a binary value.

A payload, 16 bytes to a row

Find the byte at offset 0x11 before you hover over it. Then find where the number 21.5 starts and ends.

Loading interactive…

Worked example

Reading digits out of a dump

  1. 1

    A board sends a reading and the receiver's hex dump shows, at offsets 0x05 to 0x08, the bytes 32 31 2e 35. What value was sent, and in what form?

  2. 2

    Look each byte up. 0x32 is the character 2, 0x31 is 1, 0x2e is ., 0x35 is 5. Digits in ASCII run from 0x30 for 0, so subtract 0x30 from each digit byte.

  3. 3

    Read the text. 21.5, written as four characters of text, not as a binary number.

  4. 4

    Why it matters. A receiver that tried to read those four bytes as one 32-bit number would get nonsense. The bytes are text, and must be parsed as text.

JSON

JSON is a way of writing data as text so that any program can read it. It's short for JavaScript Object Notation, but every language uses it now.

It has a handful of building blocks. Words and names go in double quotes, like "bench1". Numbers are written plainly, with no quotes and no units, like 21.5. There are also true, false and null, which means "no value."

Curly braces make an object: a set of labels, each with a value. {"t": 21.5, "rh": 64} has two labels. t holds 21.5, the temperature, and rh holds 64, the relative humidity. A colon goes between a label and its value, and commas go between pairs.

Square brackets make an array: a list in order, like [21.5, 21.6, 21.4]. Objects and arrays can sit inside each other.

Why do devices use JSON so much? The labels travel with the numbers, so the receiver knows which number is which. Every programming language can read it. And people can read it too, in a log or a hex dump.

The price is size. Those two readings take 18 bytes as JSON. If both sides agreed on a fixed layout in advance, they could fit in 3 bytes.

JSON (JavaScript Object Notation) is a text format for structured data. A JSON document is one value, and a value is one of:

  • a string in double quotes: "bench1";
  • a number, with no quotes and no units: 21.5, 64, -3;
  • true, false or null;
  • an object: { key-value pairs }, where each key is a string, a colon separates it from its value, and commas separate pairs: {"t": 21.5, "rh": 64};
  • an array: an ordered list of values in brackets: [21.5, 21.6, 21.4].

Objects and arrays nest, so {"bench": "b1", "temps": [21.5, 21.6]} is one object holding a string and an array. JSON text is UTF-8, keys are case-sensitive, and whitespace outside strings means nothing, so {"t":21.5} and { "t": 21.5 } are the same document in 10 and 13 bytes.

JSON is the most common payload in this course and in device systems generally, for three reasons. It is self-describing: the keys travel with the values, so a reader can tell t from rh without a separate format description. Every language this course uses parses it with its standard library or one well-known package (Python's json, ArduinoJson on the board). And it is readable in a hex dump and in a log.

Its cost is size. The two values above take 18 bytes as compact JSON. A binary encoding that both sides agree on in advance could send the temperature as a 16-bit integer in hundredths of a degree (2150) and the humidity as one byte: 3 bytes. CBOR (Concise Binary Object Representation) is a standard binary format with JSON's data model that sits between the two; Chapter 10 §6 compares them properly.

// Build a compact JSON payload with ArduinoJson 7 and count its bytes.
#include <ArduinoJson.h>

void setup() {
    Serial.begin(115200);
    JsonDocument doc;
    doc["t"] = 21.5;
    doc["rh"] = 64;
    char buf[64];
    size_t n = serializeJson(doc, buf, sizeof(buf));   // bytes written
    Serial.printf("%u bytes: %s\n", (unsigned)n, buf);
}

void loop() {}

On a board this should print 18 bytes: {"t":21.5,"rh":64}. The same payload in Python, on your computer, is json.dumps({"t": 21.5, "rh": 64}, separators=(",", ":")).encode("utf-8"); separators drops the spaces that json.dumps adds by default, and .encode("utf-8") turns the text into the bytes that are actually sent.

In Chapter 9 these bytes travel inside an MQTT message, the message format this course uses most. Here is one, built for this example, so you can see that the payload is the same 18 bytes.

The JSON payload inside a message

Click the payload field and find its first and last bytes in the hex. Then count: how many bytes come before the payload, and what do they hold?

Loading interactive…

Worked example

The size of a day of readings

  1. 1

    A sensor publishes {"t":21.5,"rh":64} every 10 seconds. How many payload bytes is that per day, as JSON and as the 3-byte binary layout?

  2. 2

    Messages per day. 86\,400 / 10 = 8640.

  3. 3

    JSON. 8640 \times 18 = 155\,520 bytes, about 156 kB.

  4. 4

    Binary. 8640 \times 3 = 25\,920 bytes, about 26 kB: six times less.

  5. 5

    Is it worth it? On home Wi-Fi, 156 kB a day is nothing, and JSON's readability wins. On a LoRa link limited to tens of bytes per message and seconds of airtime per hour, the difference decides whether the design works at all. The choice depends on the link, which is why Chapters 7 and 10 return to it.

Worked example

When whitespace and labels cost bytes

  1. 1

    Compare {"temperature": 21.5, "humidity": 64} with {"t":21.5,"rh":64}. Same data?

  2. 2

    Same values, but not the same document. The keys differ, so a receiver expecting t won't find temperature. Keys are part of the agreement between sender and receiver.

  3. 3

    Count the bytes. The long one is 37 bytes and the short one is 18. The 19 extra bytes are 16 more characters of key names, plus three spaces: two after the colons and one after the comma.

  4. 4

    The habit. Choose short, clear keys once, write them down, and keep the spaces out of payloads a device sends. Pretty-printed JSON is for people reading files.

🛠️ Build it yourself

Lab: a hex dump and a payload sizer in Python

A small Python package with two functions to finish: hexdump(), which prints bytes 16 to a row with offsets and a character column, and payload_size(), which counts the UTF-8 bytes of a compact JSON payload. The tests, already written, check both against the examples on this page. Nothing touches a network.

What's inside (11 files)
  • payload-bytes/.devcontainer/devcontainer.json
  • payload-bytes/.vscode/extensions.json
  • payload-bytes/.vscode/launch.json
  • payload-bytes/.vscode/settings.json
  • payload-bytes/.vscode/tasks.json
  • payload-bytes/README.md
  • payload-bytes/pyproject.toml
  • payload-bytes/src/payload_bytes/__init__.py
  • payload-bytes/src/payload_bytes/__main__.py
  • payload-bytes/src/payload_bytes/dump.py
  • payload-bytes/tests/test_dump.py

Unzip it, then make a virtual environment, install pytest with python -m pip install pytest, and run the tests with python -m pytest. The README has the steps for every OS and IDE. In VS Code, open the folder: the test task (Tasks: Run Test Task) and the debugger (F5) are already set up.

Check your understanding (counts toward your progress)

A receiver's hex dump of a payload ends 36 34 7d 0a. What is the last byte?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

Each pair of hex digits is one byte. 0a is one byte, value 10.

36 34 is 64; the brace 7d comes between it and 0a.

A closing brace is 7d, which appears just before.

Why

0x0a is the line feed. A printed string hides it; a hex dump shows it, and a strict receiver may reject it.

0a is 10, a control code, not a printable character.

A device sends readings over a link that allows a few dozen bytes per message and a few seconds of airtime per hour. Which payload format is the better starting point?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

Indentation and long keys multiply the bytes for no gain to a machine reader.

Readable, but keys travel with every message on a link where each byte counts.

Text with units is bigger than JSON's numbers and harder to parse.

Why

When the link is the bottleneck, a fixed binary layout (3 bytes for the section's reading) beats JSON's self-describing 18. On home Wi-Fi, JSON's readability usually wins.

On this link, every byte costs airtime.

In {"bench": "b1", "temps": [21.5, 21.6, 21.4]}, what is the value of temps?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

An object uses braces and key-value pairs. These are brackets and bare values.

The values aren't in quotes, so they are numbers, not text.

The brackets hold all three numbers as one ordered list.

Why

Square brackets make an array, an ordered list of values; here, three numbers.

Square brackets or curly braces?

How many bytes does the text café take in UTF-8?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

Why

c, a and f take 1 byte each; é takes 2 (C3 A9). 3 + 2 = 5 bytes for 4 characters.

Correct answer

Also accepted:

Why

Three of the letters are ASCII. One is not.

Why can a program read plain ASCII text as UTF-8 without converting it?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

UTF-8 uses those bytes: they are its one-byte characters.

Networks move bytes and never convert text.

ASCII is one byte per character, and so is UTF-8 for those same characters.

Why

UTF-8 was designed so that its first 128 codes match ASCII byte for byte. Any ASCII text is already valid UTF-8.

Compare A in both encodings.

Practice: Bytes, hex dumps and JSON (5 problems · doesn’t affect your progress)

A byte holds the value 0x41. What does it mean?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

Only if both sides agreed to read it as text.

0x41 is hex for 65, and even then it's only a number if read as one.

Encryption has nothing to do with meaning.

Why

Bytes carry no meaning on their own. The agreed encoding or format gives them one.

It depends on how the receiver reads it

How many bytes does 5 € (five, a space, the euro sign) take in UTF-8?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

Why

1 + 1 + 3 = 5 bytes for 3 characters.

Correct answer

Also accepted:

Why

The euro sign is one of the three-byte characters.

5

This is the JSON payload {"t":21.5,"rh":64}, 16 bytes to a row. Click the four bytes that hold the text 21.5, then check.

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

What this task checks

Structure checked automatically. Whether your ideas are convincing isn't graded, so reread with the checklist.

The compiler says

A model answer

Why

The number is four ASCII characters at offsets 5–8: 32 31 2e 35.

Correct answer

Also accepted:

Why

Count the bytes before the number: brace, quote, t, quote, colon.

0x0005–0x0008

How many bytes is {"temperature": 21.5, "humidity": 64} minus {"t":21.5,"rh":64}?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

Why

37 − 18 = 19: 10 more for temperature, 6 more for humidity, and 3 spaces.

Correct answer

Also accepted:

Why

The short one is 18 bytes. Count the long one, spaces included.

19

A receiver reads the four bytes 32 31 2e 35 as one 32-bit binary number and gets a huge value. What went wrong?

Correct! 🎉 Not quite.

Right on try — credit. Right — the answer had already been shown, so no credit this time. Next try is worth Answer shown after three tries — this one no longer earns credit. You chose to see the answer — this one earns no credit.

Byte order matters for binary numbers. These aren't one.

They decode cleanly as the characters 2, 1, . and 5.

Any width gives nonsense when text is read as a binary number.

Why

32 31 2e 35 is the text 21.5. Sender and receiver must agree on the format, here text inside JSON.

They are text digits and must be parsed as text

Where this goes next

Bytes and hex dumps come back in Chapter 2 §1, where packets are read layer by layer, and in Chapter 2 §8, where you capture your own device's traffic. Chapter 5 §5 builds and validates JSON on the board with ArduinoJson, and Chapter 10 §6 weighs JSON against CBOR and Protobuf. UTF-8 byte counts matter again in Chapter 9 §3, because MQTT limits topic names in bytes.

Feeling good about this section?

Take the section quiz

Every answer gets a full explanation at the end.

Sign in to save highlights

Insert an equation

  • No matches.