micropython-serializacion-json-struct-binary

Serialization in MicroPython: JSON and struct

  • 5 min

Serialization means converting data into a sequence that can be saved or sent.

When we program, we work with objects: lists, dictionaries, floating-point numbers… But when we want to save that data to a file or send it via wire/radio to another device, we cannot send the “abstract object.” We have to convert it into a linear sequence of bytes.

This process of “flattening” the data is called Serialization.

In MicroPython, we have two main ways to do this, and choosing the right one defines your project’s performance:

  1. JSON: Human-readable, web standard, but “heavy.”
  2. Struct: Pure binary, unreadable to humans, but extremely compact and fast.

JSON: The Universal Standard

We already saw in the previous chapter how to save configurations in JSON. The ujson (or simply json) library converts Python dictionaries and lists into a formatted text string.

It is perfect for:

  • Exchanging data with web servers (REST APIs).
  • Saving readable configuration (config.json).
  • Sending data via MQTT where bandwidth is not critical.

Serializing and Deserializing

import json

# We have a dictionary with mixed types
data = {
    "sensor": "DHT22",
    "id": 12,
    "values": [23.5, 60.2],
    "active": True
}

# 1. Serialize to Text (String)
message_txt = json.dumps(data)
print(f"Sending: {message_txt}")
# Output: {"sensor": "DHT22", "id": 12, "valores": [23.5, 60.2], "activo": true}
# Takes up about ~80 bytes

# 2. Deserialize (Back to object)
received = json.loads(message_txt)
print(received["values"][0]) # 23.5
Copied!

The cost of text Notice that the number 23.5 in memory takes 4 bytes (float). But in JSON it is transmitted as the characters '2', '3', '.', '5', also taking 4 bytes… plus the quotes, brackets, and keys {"valores":...}. To send a small piece of data, JSON adds a lot of “junk” (overhead).

struct: Compact Binary Data

Imagine you want to send temperature (23.5) and humidity (60) over a LoRaWAN or ESP-NOW network, where every byte counts and bandwidth is gold.

Sending the text {"temp": 23.5, "hum": 60} consumes about 25 bytes. If we send it in pure binary (as C does), it would only take 5 or 6 bytes.

For this, we use the ustruct (or struct) module. This module allows us to pack data into byte blocks following C format.

The Format String

The magic of struct lies in defining how we want to pack the data using special characters:

  • b / B: Signed / Unsigned byte (1 byte). (For small integers 0-255).
  • h / H: Short integer (2 bytes). (For integers up to 65535).
  • i / I: Integer (4 bytes). (Large integers).
  • f: Float (4 bytes). (Standard decimals).
  • s: String (Fixed bytes).

Packing Sensor Data

We want to send:

  1. Sensor ID (Positive integer, max 255) -> 1 byte (B).
  2. Temperature (Float) -> 4 bytes (f).
  3. Humidity (Integer) -> 2 bytes (h).

Total: 1 + 4 + 2 = 7 bytes. (Compared to >25 for JSON).

import struct

# Data to send
sensor_id = 101
temp = 23.45
hum = 60

# 1. Pack
# The '>' indicates Big-Endian (network standard)
# 'B f h' is the format: Byte, Float, Short
binary_packet = struct.pack('>Bfh', sensor_id, temp, hum)

print(f"Bytes: {binary_packet}")
print(f"Length: {len(binary_packet)} bytes")
# Visual output unreadable type: b'eA\xbb\x99\x9a<'
Copied!

Unpacking Data

The receiver (another ESP32 or a Python server) receives that byte stream and, knowing the format, reconstructs the data.

# Reception simulation
received_data = b'eA\xbb\x99\x9a<'

# 2. Unpack
# Always returns a TUPLE
values = struct.unpack('>Bfh', received_data)

print(f"ID: {values[0]}")
print(f"Temp: {values[1]:.2f}") # Floats lose a tiny bit of precision
print(f"Hum: {values[2]}")
Copied!

Endianness: The Order of Bytes

Did you notice the > symbol at the beginning of the format? It defines the Endianness (the order of bytes).

  • > Big Endian: The most significant byte comes first. It is the standard in networks (Internet, TCP/IP).
  • < Little Endian: The least significant byte comes first. It is the standard in Intel processors and many ARM/ESP32 microcontrollers.

Key Idea: If you send data between two identical machines (ESP32 to ESP32), it doesn’t matter. But if you send data over a network or to a different server, always use > (Big Endian) or ! (Network) to ensure compatibility.

Reducing Size with Fixed Point

Sometimes, even 4 bytes for a float seems like a lot. A very common trick in IoT is to multiply by 100 and send an integer.

  • Data: 23.45
  • Multiplied x100: 2345
  • Packed as short (h): 2 bytes.

Upon receiving, we divide by 100. We have saved half the space without losing practical precision.

JSON or struct

FeatureJSON (ujson)Binary (ustruct)
ReadabilityHuman (Text)Machine (Weird Bytes)
SizeLarge (Verbose)Minimal (Compact)
CPU SpeedSlow (Parsing text)Very Fast (Memory copy)
Ideal UseWeb, MQTT, Configuration, DebugLoRaWAN, ESP-NOW, Large Files

Mastering struct is what allows you to say: “I have created my own optimized communication protocol”. It is an indispensable tool in the embedded developer’s belt.