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: tuple Python dikonversi menjadi array JSON, tapi saat dibaca kembali menjadi list Python — bukan tuple. 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 \uXXXX yang 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() vs json.load()loads() dari string, load() dari file object. Jangan tertukar.
  • json.dumps() vs json.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 default atau subclass JSONEncoder untuk serialize datetime, 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 tidak KeyError jika struktur berubah.

← Sebelumnya: Mocking   Berikutnya: YAML →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact