Structured Data Serialization: JSON, CSV, and TOML
Structured Data Serialization: JSON, CSV, and TOML
After reading this chapter, you will master Python’s built-in serialization libraries: encoding and decoding JSON payloads with custom type hooks, processing structured tabular datasets with csv.DictReader and csv.DictWriter, and parsing configuration files using the built-in tomllib parser.
Mental model
Serialization translates in-memory Python objects (dictionaries, lists, primitives) into standardized byte or text streams suitable for disk persistence or network transmission:
Python Memory Standard Library Module Serialized Format
─────────────────────────────────────────────────────────────────────────────
dict / list / primitives ──▶ json.dumps() ──▶ JSON text string
tabular record list ──▶ csv.DictWriter() ──▶ Comma-Separated Values
TOML configuration text ──▶ tomllib.loads() ──▶ Python dict
While JSON supports only a subset of primitives (str, int, float, bool, list, dict, None), Python provides extension hooks (default= and object_hook=) to encode domain types like datetime, UUID, or set.
Minimal example
Save as serialization_showcase.py:
# serialization_showcase.py
import csv
import io
import json
import tomllib
def main() -> None:
# 1. JSON Serialization
cluster_state = {
"cluster": "k8s-prod-us",
"nodes": 3,
"active": True,
"tags": ["web", "api"],
}
json_str = json.dumps(cluster_state, indent=2)
print("--- Serialized JSON ---")
print(json_str)
# 2. TOML Parsing (Built-in via tomllib in Python 3.11+)
raw_toml = """
[server]
host = "0.0.0.0"
port = 8080
[database]
pool_size = 10
timeout_seconds = 5.5
"""
config_dict = tomllib.loads(raw_toml)
print("\n--- Parsed TOML Configuration ---")
print(f"Host: {config_dict['server']['host']}:{config_dict['server']['port']}")
print(f"DB Timeout: {config_dict['database']['timeout_seconds']}s")
# 3. CSV Tabular Generation with DictWriter
csv_buffer = io.StringIO()
fieldnames = ["hostname", "ip", "status"]
writer = csv.DictWriter(csv_buffer, fieldnames=fieldnames)
writer.writeheader()
writer.writerow({"hostname": "node-01", "ip": "10.0.1.10", "status": "UP"})
writer.writerow({"hostname": "node-02", "ip": "10.0.1.11", "status": "UP"})
print("\n--- Generated CSV ---")
print(csv_buffer.getvalue().strip())
if __name__ == "__main__":
main()Run via uv run python serialization_showcase.py:
--- Serialized JSON ---
{
"cluster": "k8s-prod-us",
"nodes": 3,
"active": true,
"tags": [
"web",
"api"
]
}
--- Parsed TOML Configuration ---
Host: 0.0.0.0:8080
DB Timeout: 5.5s
--- Generated CSV ---
hostname,ip,status
node-01,10.0.1.10,UP
node-02,10.0.1.11,UP
Worked examples
Case 1: Custom JSON Serialization for datetime, UUID, and set
Attempting to serialize non-native types raises TypeError. Supplying a custom default= callable serializes extended types cleanly:
# custom_json_encoder.py
import json
from datetime import datetime, timezone
from uuid import UUID, uuid4
from typing import Any
def custom_json_serializer(obj: Any) -> Any:
if isinstance(obj, (datetime)):
return obj.isoformat()
if isinstance(obj, UUID):
return str(obj)
if isinstance(obj, set):
return sorted(list(obj))
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
if __name__ == "__main__":
telemetry_payload = {
"event_id": uuid4(),
"timestamp": datetime.now(timezone.utc),
"tags": {"prod", "us-east-1", "critical"},
"metric": 98.4,
}
serialized = json.dumps(telemetry_payload, indent=2, default=custom_json_serializer)
print("Serialized payload with custom types:")
print(serialized)Run:
uv run python custom_json_encoder.pyOutput:
Serialized payload with custom types:
{
"event_id": "...",
"timestamp": "2026-09-07T...",
"tags": [
"critical",
"prod",
"us-east-1"
],
"metric": 98.4
}
Case 2: Reading CSV with DictReader and Type Conversion
csv.reader returns raw string lists (["10.0.1.1", "8080"]). DictReader binds values to header column names:
# parse_nodes_csv.py
import csv
import io
RAW_CSV_DATA = """hostname,port,healthy
app-node-01,8080,true
app-node-02,8080,false
db-primary,5432,true
"""
def parse_node_inventory(csv_text: str) -> list[dict[str, str | int | bool]]:
stream = io.StringIO(csv_text.strip())
reader = csv.DictReader(stream)
nodes = []
for row in reader:
nodes.append({
"hostname": row["hostname"],
"port": int(row["port"]),
"healthy": row["healthy"].strip().lower() == "true",
})
return nodes
if __name__ == "__main__":
inventory = parse_node_inventory(RAW_CSV_DATA)
print("Parsed and typed node inventory:")
for node in inventory:
print(f" {node['hostname']:15} | Port: {node['port']:5d} | Healthy: {node['healthy']}")Run:
uv run python parse_nodes_csv.pyOutput:
Parsed and typed node inventory:
app-node-01 | Port: 8080 | Healthy: True
app-node-02 | Port: 8080 | Healthy: False
db-primary | Port: 5432 | Healthy: True
Pitfalls
Pitfall 1: Writing CSVs on Windows Without newline=""
When opening a file for csv.writer, failing to pass newline="" causes Python and the CSV module to double-write carriage returns (\r\r\n), resulting in blank lines between every record:
# THE BUG:
with open("output.csv", "w") as f:
writer = csv.writer(f)
# THE FIX:
with open("output.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)Pitfall 2: tomllib is Read-Only
The standard library tomllib module (PEP 680) provides high-performance TOML parsing only (loads and load). If your application needs to write TOML files, use the third-party package tomli_w or serialize to YAML/JSON.
Exercises
- Write a script that reads a JSON file, modifies a nested key, and writes it back atomically using formatted indentation (
indent=2). - Create a custom JSON decoder using
object_hookthat automatically converts ISO-8601 string timestamps back intodatetimeobjects. - Parse a CSV file containing user IDs, filter out inactive users, and write the remaining users to a new CSV using
csv.DictWriter. - Parse a
pyproject.tomlfile withtomllib.loads()and print the project name and dependencies list.
Further reading
- PEP 680: tomllib: Support for Parsing TOML in the Standard Library.
- Python Standard Library:
json,csv, andtomllibmodules. - JSON Specification: RFC 8259.