JSON adalah bahasa data internet: respons API, event analytics, berkas konfigurasi, sampai log aplikasi modern semuanya berformat JSON. Berbeda dengan CSV, JSON mengenal angka, boolean, dan struktur bersarang — tetapi kekuatan itu sendiri yang menimbulkan masalah baru: field bisa hilang di satu record, angka bisa tersimpan sebagai string, dan data bersarang harus diratakan sebelum bisa dianalisis sebagai tabel. Chapter Membaca dan Menulis JSON telah membahas json.load() dan json.loads(); chapter ini berfokus pada pola membaca data JSON dalam skenario pengolahan data yang sesungguhnya.
Daftar Record dengan json.load #
Bentuk paling umum untuk data tabular adalah list of dictionary, sering disebut records. Simpan berikut sebagai produk.json — dua record terakhir sengaja dibuat bermasalah:
[
{"id": "P001", "nama": "Kopi Gayo", "harga": 85000, "kategori": "minuman", "stok": 120},
{"id": "P002", "nama": "Teh Melati", "harga": 45000, "kategori": "minuman", "stok": 200},
{"id": "P003", "nama": "Gula Aren", "harga": 32000, "kategori": "bahan pokok", "stok": 75},
{"id": "P005", "nama": "Keripik Singkong", "harga": 15000, "kategori": "camilan", "stok": 250},
{"id": "P006", "nama": "Sirup Markisa", "harga": 22000, "kategori": "minuman", "stok": 60},
{"id": "P008", "nama": "Mie Instan Goreng", "harga": 3500, "kategori": "bahan pokok", "stok": 500},
{"id": "P009", "nama": "Susu Kental Manis", "harga": 14500, "kategori": "minuman", "stok": 80},
{"id": "P012", "nama": "Cangkir Keramik", "harga": 55000, "kategori": "aksesoris", "stok": 25},
{"id": "P007", "nama": "Sambal Bawang", "harga": "28000", "kategori": "camilan", "stok": 90},
{"id": "P010", "nama": "Kopi Luwak", "harga": 250000, "stok": 10}
]Membacanya adalah satu panggilan json.load(), dan angka langsung menjadi int — tidak ada tahap konversi seperti pada CSV:
import json
with open("produk.json", encoding="utf-8") as berkas:
produk = json.load(berkas)
print(f"Jumlah record : {len(produk)}")
print(f"Record pertama: {produk[0]}")
print(f"harga + 1000 : {produk[0]['harga'] + 1000}")Output yang Dihasilkan:
Jumlah record : 10
Record pertama: {'id': 'P001', 'nama': 'Kopi Gayo', 'harga': 85000, 'kategori': 'minuman', 'stok': 120}
harga + 1000 : 86000Validasi Struktur Sebelum Diproses #
JSON tidak punya skema wajib — pembuat data bisa lupa satu field, atau menyimpan angka dalam tanda kutip. Record P007 (harga string) dan P010 (kategori hilang) di atas contoh nyatanya. Sebelum data dipakai, kita periksa struktur setiap record:
KOLOM_WAJIB = {"id", "nama", "harga", "kategori"}
def periksa_rekam(rekam):
if not KOLOM_WAJIB.issubset(rekam):
return "kolom tidak lengkap"
if not isinstance(rekam["harga"], (int, float)):
return "harga bukan angka"
return None
produk_valid = []
for rekam in produk:
masalah = periksa_rekam(rekam)
if masalah is None:
produk_valid.append(rekam)
else:
print(f" {rekam['id']}: {masalah}")
print(f"Valid: {len(produk_valid)} dari {len(produk)} record")
rata_harga = sum(p["harga"] for p in produk_valid) / len(produk_valid)
print(f"Harga rata-rata: {rata_harga:,.0f}".replace(",", "."))Output yang Dihasilkan:
P007: harga bukan angka
P010: kolom tidak lengkap
Valid: 8 dari 10 record
Harga rata-rata: 34.000Method issubset() memeriksa keberadaan seluruh kolom wajib sekaligus, dan isinstance() memastikan tipe data sesuai harapan. Tanpa pemeriksaan ini, sum() akan melempar TypeError begitu bertemu string "28000" — atau lebih buruk, lolos diam-diam kalau data hanya dipakai untuk ditampilkan.
NDJSON: Satu Objek per Baris #
Newline-delimited JSON (NDJSON) menulis satu objek JSON per baris. Format ini populer untuk log event dan stream karena bisa dibaca bertahap tanpa memuat seluruh array. Simpan berikut sebagai events.ndjson — baris terakhir sengaja dipotong:
{"event": "view_product", "sku": "P001", "user": "budi", "ts": "2025-06-15T08:03:12"}
{"event": "view_product", "sku": "P002", "user": "siti", "ts": "2025-06-15T08:05:44"}
{"event": "add_to_cart", "sku": "P001", "user": "budi", "ts": "2025-06-15T08:06:02"}
{"event": "view_product", "sku": "P005", "user": "rudi", "ts": "2025-06-15T08:09:30"}
{"event": "add_to_cart", "sku": "P005", "user": "rudi", "ts": "2025-06-15T08:10:15"}
{"event": "checkout", "user": "budi", "total": 170000, "ts": "2025-06-15T08:12:40"}
{"event": "view_product", "sku": "P001", "user": "siti", "ts": "2025-06-15T08:15:21"}
{"event": "add_to_cart", "sku": "P005", "user": "siti", "ts": "2025-06-15T08:16:03"}
{"event": "checkout", "user": "rudi"Setiap baris diurai terpisah dengan json.loads(). Karena satu baris bisa rusak, pembacaan dibungkus try/except per baris — pola yang sama dengan baris rusak pada CSV:
import json
from collections import Counter
event_valid = []
with open("events.ndjson", encoding="utf-8") as berkas:
for nomor, baris in enumerate(berkas, start=1):
try:
event_valid.append(json.loads(baris))
except json.JSONDecodeError as galat:
print(f"Baris {nomor} dilewati: {galat}")
print(f"{len(event_valid)} event valid")
print(Counter(e["event"] for e in event_valid))Output yang Dihasilkan:
Baris 9 dilewati: Expecting ',' delimiter: line 1 column 37 (char 36)
8 event valid
Counter({'view_product': 4, 'add_to_cart': 3, 'checkout': 1})Untuk file event berukuran besar, bungkus pembacaan ini dalam generator. Hanya satu baris yang berada di memori pada satu waktu — materi chapter yield dan Generator Functions:
def baca_ndjson(nama_berkas):
with open(nama_berkas, encoding="utf-8") as berkas:
for baris in berkas:
try:
yield json.loads(baris)
except json.JSONDecodeError:
continue # baris rusak dilewati
for event in baca_ndjson("events.ndjson"):
if event["event"] == "add_to_cart":
print(f"{event['user']} menambahkan {event['sku']}")Output yang Dihasilkan:
budi menambahkan P001
rudi menambahkan P005
siti menambahkan P005Meratakan JSON Bersarang #
Data nyata sering bersarang: pesanan memuat pelanggan, alamat pengiriman, dan daftar item. Simpan berikut sebagai pesanan.json:
{
"id_pesanan": "ORD-2041",
"status": "dikirim",
"pelanggan": {"nama": "Budi Santoso", "email": "[email protected]"},
"pengiriman": {
"alamat": "Jl. Merdeka No. 10",
"kota": "Bandung",
"kode_pos": "40115",
"kurir": "Reguler"
},
"item": [
{"sku": "P001", "nama": "Kopi Gayo", "qty": 2, "harga": 85000},
{"sku": "P005", "nama": "Keripik Singkong", "qty": 3, "harga": 15000}
]
}Fungsi flatten() rekursif berikut meratakan dictionary bersarang menjadi kunci bertitik, sambil membiarkan list tetap utuh:
import json
def flatten(data, prefiks=""):
hasil = {}
for kunci, nilai in data.items():
kunci_baru = f"{prefiks}.{kunci}" if prefiks else kunci
if isinstance(nilai, dict):
hasil.update(flatten(nilai, kunci_baru))
else:
hasil[kunci_baru] = nilai
return hasil
with open("pesanan.json", encoding="utf-8") as berkas:
pesanan = json.load(berkas)
for kunci, nilai in flatten(pesanan).items():
print(f"{kunci}: {nilai!r}")Output yang Dihasilkan:
id_pesanan: 'ORD-2041'
status: 'dikirim'
pelanggan.nama: 'Budi Santoso'
pelanggan.email: '[email protected]'
pengiriman.alamat: 'Jl. Merdeka No. 10'
pengiriman.kota: 'Bandung'
pengiriman.kode_pos: '40115'
pengiriman.kurir: 'Reguler'
item: [{'sku': 'P001', 'nama': 'Kopi Gayo', 'qty': 2, 'harga': 85000}, {'sku': 'P005', 'nama': 'Keripik Singkong', 'qty': 3, 'harga': 15000}]Mari kita bedah logikanya:
- Baris 4 (
for kunci, nilai in data.items()): fungsi berjalan per pasangan kunci-nilai pada satu tingkat dictionary. - Baris 5 (
kunci_baru = f"{prefiks}.{kunci}" ...): kunci anak digabung dengan kunci induk menjadipelanggan.nama; pada panggilan pertamaprefiksmasih kosong sehingga kunci tetapid_pesanan. - Baris 6-7 (
if isinstance(nilai, dict)): jika nilainya masih dictionary, fungsi memanggil dirinya sendiri dengan prefiks yang lebih dalam — di sinilah rekursinya bekerja. - Baris 8-9 (
hasil[kunci_baru] = nilai)): nilai non-dictionary (termasuk listitem) langsung disimpan; list dibiarkan utuh karena isinya records yang akan diperlakukan terpisah.
pandas menyediakan operasi yang sama lewat pd.json_normalize(), dengan nama kolom bertitik otomatis:
import pandas as pd
df = pd.json_normalize([pesanan])
print(df.columns.tolist())
print(df[["pelanggan.nama", "pengiriman.kota"]].to_string(index=False))Output yang Dihasilkan:
['id_pesanan', 'status', 'item', 'pelanggan.nama', 'pelanggan.email', 'pengiriman.alamat', 'pengiriman.kota', 'pengiriman.kode_pos', 'pengiriman.kurir']
pelanggan.nama pengiriman.kota
Budi Santoso BandungGalat JSON yang Nyata #
Kesalahan paling umum adalah koma buntut (trailing comma) dan tanda kutip tunggal — keduanya sah di Python tetapi ilegal di JSON:
import json
try:
json.loads('{"nama": "Budi", "harga": 45000,}')
except json.JSONDecodeError as galat:
print(type(galat).__name__, "-", galat)Output yang Dihasilkan:
JSONDecodeError - Expecting property name enclosed in double quotes: line 1 column 33 (char 32)Dengan pola records, validasi struktur, NDJSON, dan flattening di tangan, kita siap menghadapi format data kantor yang paling umum berikutnya: file Excel.