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:

json
[
  {"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:

python
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:

text
Jumlah record : 10
Record pertama: {'id': 'P001', 'nama': 'Kopi Gayo', 'harga': 85000, 'kategori': 'minuman', 'stok': 120}
harga + 1000  : 86000

Validasi 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:

python
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:

text
  P007: harga bukan angka
  P010: kolom tidak lengkap
Valid: 8 dari 10 record
Harga rata-rata: 34.000

Method 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:

text
{"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:

python
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:

text
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:

python
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:

text
budi menambahkan P001
rudi menambahkan P005
siti menambahkan P005

Meratakan JSON Bersarang #

Data nyata sering bersarang: pesanan memuat pelanggan, alamat pengiriman, dan daftar item. Simpan berikut sebagai pesanan.json:

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:

python
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:

text
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:

  1. Baris 4 (for kunci, nilai in data.items()): fungsi berjalan per pasangan kunci-nilai pada satu tingkat dictionary.
  2. Baris 5 (kunci_baru = f"{prefiks}.{kunci}" ...): kunci anak digabung dengan kunci induk menjadi pelanggan.nama; pada panggilan pertama prefiks masih kosong sehingga kunci tetap id_pesanan.
  3. Baris 6-7 (if isinstance(nilai, dict)): jika nilainya masih dictionary, fungsi memanggil dirinya sendiri dengan prefiks yang lebih dalam — di sinilah rekursinya bekerja.
  4. Baris 8-9 (hasil[kunci_baru] = nilai)): nilai non-dictionary (termasuk list item) 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:

python
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:

text
['id_pesanan', 'status', 'item', 'pelanggan.nama', 'pelanggan.email', 'pengiriman.alamat', 'pengiriman.kota', 'pengiriman.kode_pos', 'pengiriman.kurir']
pelanggan.nama pengiriman.kota
  Budi Santoso         Bandung

Galat JSON yang Nyata #

Kesalahan paling umum adalah koma buntut (trailing comma) dan tanda kutip tunggal — keduanya sah di Python tetapi ilegal di JSON:

python
import json

try:
    json.loads('{"nama": "Budi", "harga": 45000,}')
except json.JSONDecodeError as galat:
    print(type(galat).__name__, "-", galat)

Output yang Dihasilkan:

text
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.