JSON #
JSON (JavaScript Object Notation) adalah format pertukaran data yang paling banyak digunakan dalam pengembangan perangkat lunak modern — mulai dari response API, file konfigurasi, hingga komunikasi antar service. Python menyediakan modul json bawaan yang lengkap untuk membaca, menulis, dan memanipulasi data JSON tanpa perlu instalasi library tambahan. Memahami cara kerja modul ini secara mendalam — termasuk pemetaan tipe data, penanganan error, dan pola serialisasi objek kustom — akan menghindarkan kamu dari bug yang sulit dilacak di lingkungan produksi.
Pemetaan Tipe Data Python ↔ JSON #
Sebelum memulai, penting memahami bagaimana Python dan JSON saling memetakan tipe datanya. Konversi yang tidak sesuai harapan sering menjadi sumber bug yang tidak terlihat langsung.
Python → JSON
──────────────────────────
dict → object {}
list, tuple → array []
str → string ""
int, float → number
True → true
False → false
None → null
JSON → Python
──────────────────────────
object {} → dict
array [] → list
string "" → str
number (int) → int
number (float) → float
true → True
false → False
null → None
Perhatikan:tuplePython dikonversi menjadiarrayJSON, tapi saat dibaca kembali menjadilistPython — bukantuple. Jika urutan tipe data penting, jangan andalkan round-trip JSON untuk menjaga tipe tuple.
Parsing JSON #
Modul json menyediakan dua fungsi untuk membaca data JSON: loads() untuk string dan load() untuk file. Perbedaannya kecil tapi sering tertukar.
import json
# json.loads() -- dari string JSON
json_string = '{"nama": "Budi", "usia": 28, "aktif": true}'
data = json.loads(json_string)
print(data["nama"]) # Budi
print(data["aktif"]) # True (bukan "true" -- sudah jadi bool Python)
print(type(data)) # <class 'dict'>
# json.load() -- dari file
with open("data.json", "r", encoding="utf-8") as f:
data = json.load(f)
# ANTI-PATTERN: membaca file JSON tanpa encoding eksplisit
with open("data.json", "r") as f: # ✗ -- gagal di Windows jika ada karakter non-ASCII
data = json.load(f)
# BENAR: selalu sertakan encoding="utf-8"
with open("data.json", "r", encoding="utf-8") as f: # ✓
data = json.load(f)
Serialisasi ke JSON #
Dua fungsi untuk menghasilkan JSON: dumps() menghasilkan string, dump() langsung menulis ke file.
import json
data = {
"nama": "Budi",
"usia": 28,
"hobi": ["membaca", "coding"],
"aktif": True,
"alamat": None
}
# Ke string JSON (compact)
json_compact = json.dumps(data)
print(json_compact)
# {"nama": "Budi", "usia": 28, "hobi": ["membaca", "coding"], "aktif": true, "alamat": null}
# Ke string JSON (pretty print)
json_pretty = json.dumps(data, indent=2, sort_keys=True, ensure_ascii=False)
print(json_pretty)
# {
# "aktif": true,
# "alamat": null,
# "hobi": ["membaca", "coding"],
# "nama": "Budi",
# "usia": 28
# }
# Langsung ke file
with open("output.json", "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
ensure_ascii=False— parameter ini penting jika datamu mengandung karakter non-ASCII seperti huruf beraksen, karakter Arab, atau karakter Indonesia. Tanpanya, karakter tersebut akan di-escape menjadi\uXXXXyang tetap valid JSON tapi sulit dibaca.
Penanganan Error #
Parsing JSON dari sumber eksternal (API, file user-upload, input form) harus selalu dibungkus penanganan error. Data yang tidak valid harus ditangani secara eksplisit, bukan dibiarkan crash.
import json
# ANTI-PATTERN: parsing tanpa error handling
data = json.loads(user_input) # ✗ -- crash jika input tidak valid JSON
# BENAR: tangani JSONDecodeError secara eksplisit
def parse_json_safe(raw: str) -> dict | None:
try:
return json.loads(raw)
except json.JSONDecodeError as e:
print(f"JSON tidak valid: {e.msg} (baris {e.lineno}, kolom {e.colno})")
return None
# Contoh penggunaan
hasil = parse_json_safe('{"nama": "Budi"') # JSON tidak lengkap
# Output: JSON tidak valid: Expecting ',' delimiter (baris 1, kolom 16)
# Returns: None
hasil = parse_json_safe('{"nama": "Budi"}')
# Returns: {"nama": "Budi"}
# Validasi sebelum memproses
def is_valid_json(raw: str) -> bool:
try:
json.loads(raw)
return True
except json.JSONDecodeError:
return False
print(is_valid_json('{"a": 1}')) # True
print(is_valid_json('{a: 1}')) # False -- key tanpa tanda kutip bukan JSON valid
print(is_valid_json('null')) # True -- null adalah JSON valid
print(is_valid_json('')) # False
Serialisasi Objek Kustom #
Secara default, json.dumps() hanya bisa menghandle tipe bawaan Python. Objek kelas kustom, datetime, Decimal, dan set akan menyebabkan TypeError. Ada dua cara mengatasinya.
Menggunakan Parameter default
#
import json
from datetime import datetime, date
from decimal import Decimal
# ANTI-PATTERN: langsung serialize objek yang tidak didukung
from datetime import datetime
data = {"waktu": datetime.now()}
json.dumps(data) # ✗ -- TypeError: Object of type datetime is not JSON serializable
# BENAR: gunakan fungsi default
def json_serializer(obj):
if isinstance(obj, (datetime, date)):
return obj.isoformat()
if isinstance(obj, Decimal):
return float(obj)
if isinstance(obj, set):
return list(obj)
raise TypeError(f"Tipe {type(obj).__name__} tidak bisa diserialisasi")
data = {
"nama": "Budi",
"dibuat": datetime(2024, 3, 15, 10, 30),
"harga": Decimal("99999.99"),
"tag": {"python", "backend"}
}
hasil = json.dumps(data, default=json_serializer, indent=2, ensure_ascii=False)
print(hasil)
# {
# "nama": "Budi",
# "dibuat": "2024-03-15T10:30:00",
# "harga": 99999.99,
# "tag": ["python", "backend"]
# }
Menggunakan Custom JSONEncoder #
Untuk proyek yang lebih besar, lebih rapi menggunakan subclass JSONEncoder:
import json
from datetime import datetime
from decimal import Decimal
class AppJSONEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
if isinstance(obj, Decimal):
return float(obj)
if isinstance(obj, set):
return sorted(list(obj)) # sort agar output konsisten
return super().default(obj)
data = {
"transaksi_id": "TRX-001",
"waktu": datetime(2024, 3, 15, 10, 30),
"total": Decimal("150000.00"),
"kategori": {"makanan", "minuman"}
}
hasil = json.dumps(data, cls=AppJSONEncoder, indent=2)
print(hasil)
Deserialisasi ke Objek Kustom #
object_hook memungkinkan kamu mengubah setiap dict yang diparse dari JSON menjadi objek Python kustom. Ini berguna untuk langsung mendapat objek yang kaya method, bukan dict mentah.
import json
from datetime import datetime
from dataclasses import dataclass
@dataclass
class Produk:
nama: str
harga: float
stok: int
def dict_to_produk(d: dict):
# Hanya konversi jika semua field yang dibutuhkan ada
if {"nama", "harga", "stok"}.issubset(d.keys()):
return Produk(
nama=d["nama"],
harga=d["harga"],
stok=d["stok"]
)
return d
json_string = '{"nama": "Laptop Gaming", "harga": 15000000.0, "stok": 5}'
produk = json.loads(json_string, object_hook=dict_to_produk)
print(type(produk)) # <class 'Produk'>
print(produk.nama) # Laptop Gaming
print(produk.harga) # 15000000.0
Serialisasi Dataclass #
Sejak Python 3.7, dataclass adalah cara modern mendefinisikan data class. Untuk serialisasi ke JSON, gunakan dataclasses.asdict():
import json
from dataclasses import dataclass, asdict, field
from typing import List
@dataclass
class Alamat:
kota: str
provinsi: str
@dataclass
class Pengguna:
nama: str
usia: int
email: str
alamat: Alamat
hobi: List[str] = field(default_factory=list)
pengguna = Pengguna(
nama="Budi Santoso",
usia=28,
email="[email protected]",
alamat=Alamat(kota="Bandung", provinsi="Jawa Barat"),
hobi=["membaca", "hiking"]
)
# Konversi ke dict dulu, baru ke JSON
data_dict = asdict(pengguna)
json_string = json.dumps(data_dict, indent=2, ensure_ascii=False)
print(json_string)
# {
# "nama": "Budi Santoso",
# "usia": 28,
# "email": "[email protected]",
# "alamat": {
# "kota": "Bandung",
# "provinsi": "Jawa Barat"
# },
# "hobi": ["membaca", "hiking"]
# }
Pola Umum dalam Aplikasi Nyata #
Membaca Konfigurasi dari File JSON #
import json
import os
from pathlib import Path
def load_config(config_path: str = "config.json") -> dict:
path = Path(config_path)
if not path.exists():
raise FileNotFoundError(f"File konfigurasi tidak ditemukan: {config_path}")
with open(path, "r", encoding="utf-8") as f:
try:
config = json.load(f)
except json.JSONDecodeError as e:
raise ValueError(f"Format config.json tidak valid: {e}")
return config
# config.json
# {
# "database": {
# "host": "localhost",
# "port": 5432,
# "name": "myapp"
# },
# "debug": false,
# "allowed_hosts": ["localhost", "127.0.0.1"]
# }
config = load_config()
db_host = config["database"]["host"] # localhost
Menyimpan dan Membaca Data Cache #
import json
from pathlib import Path
from datetime import datetime
CACHE_FILE = Path("cache.json")
def save_cache(data: dict) -> None:
payload = {
"timestamp": datetime.now().isoformat(),
"data": data
}
with open(CACHE_FILE, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2, ensure_ascii=False)
def load_cache() -> dict | None:
if not CACHE_FILE.exists():
return None
with open(CACHE_FILE, "r", encoding="utf-8") as f:
try:
payload = json.load(f)
return payload.get("data")
except json.JSONDecodeError:
CACHE_FILE.unlink() # hapus cache yang corrupt
return None
Mengolah Response API JSON #
import json
# Simulasi response dari API
api_response = '''
{
"status": "success",
"data": {
"users": [
{"id": 1, "nama": "Budi", "aktif": true},
{"id": 2, "nama": "Sari", "aktif": false}
],
"total": 2
}
}
'''
response = json.loads(api_response)
# ANTI-PATTERN: akses nested dict tanpa guard
nama = response["data"]["users"][0]["nama"] # ✗ -- KeyError jika struktur berubah
# BENAR: gunakan .get() untuk akses yang aman
users = response.get("data", {}).get("users", [])
aktif_users = [u for u in users if u.get("aktif", False)]
for user in aktif_users:
print(f"ID: {user.get('id')}, Nama: {user.get('nama')}")
# Output: ID: 1, Nama: Budi
Kapan Tidak Menggunakan Modul json Bawaan
#
Tetap gunakan modul json bawaan jika:
✓ Bekerja dengan struktur JSON standar
✓ Tidak ada kebutuhan performa ekstrem
✓ Ingin zero dependency tambahan
✓ Serialisasi/deserialisasi data sederhana
Pertimbangkan library alternatif jika:
✗ Butuh validasi schema JSON (gunakan jsonschema)
✗ Butuh performa tinggi pada parsing JSON besar (gunakan orjson atau ujson)
✗ Bekerja dengan Pydantic models (gunakan model.model_dump_json())
✗ Butuh serialisasi otomatis class tanpa boilerplate (gunakan cattrs atau marshmallow)
Ringkasan #
json.loads()vsjson.load()—loads()dari string,load()dari file object. Jangan tertukar.json.dumps()vsjson.dump()—dumps()menghasilkan string,dump()menulis langsung ke file.- Selalu
encoding="utf-8"— sertakan saat membuka file JSON untuk menghindari masalah karakter non-ASCII di berbagai OS.ensure_ascii=False— gunakan agar karakter Indonesia/non-Latin tidak di-escape menjadi\uXXXX.JSONDecodeError— selalu wrap parsing dari sumber eksternal dalam try/except untuk menangani input tidak valid.- Custom encoder — gunakan parameter
defaultatau subclassJSONEncoderuntuk serializedatetime,Decimal,set, atau objek kustom.object_hook— gunakan untuk otomatis mengonversi dict hasil parse menjadi objek Python kustom.dataclasses.asdict()— cara paling bersih untuk serialize dataclass ke JSON..get()saat akses nested — lindungi akses dict dari JSON eksternal dengan.get()agar tidakKeyErrorjika struktur berubah.