Python

Python JSON Processing

Tutorial lengkap Python JSON processing — json module, serialization, custom encoder/decoder, JSON Schema, dan best practices untuk data exchange modern

1. Pengenalan JSON

JSON (JavaScript Object Notation) adalah format pertukaran data yang ringan dan mudah dibaca. JSON menjadi standar de facto untuk komunikasi data antara client-server, API, dan konfigurasi aplikasi.

JSON vs Python Data Types

JSON Type Contoh JSON Python Type
Object{"key": "value"}dict
Array[1, 2, 3]list
String"hello"str
Number (int)42int
Number (float)3.14float
Booleantrue / falseTrue / False
NullnullNone

Mengapa JSON Penting?

Keunggulan Penjelasan
UniversalDidukung oleh hampir semua bahasa pemrograman
Human-readableFormat teks yang mudah dibaca manusia
LightweightLebih ringan dibanding XML
API StandardFormat default untuk REST API dan web services
Nested SupportBisa mengandung objek bersarang dan array
Diagram: JSON ↔ Python Mapping
┌──────────────────────────────────────────────────────┐
│             JSON ↔ PYTHON CONVERSION                  │
│                                                       │
│   JSON (string)          Python (objects)             │
│   ────────────           ────────────────             │
│   '{"name":"Budi"}'  ←→  {"name": "Budi"}            │
│   '[1, 2, 3]'        ←→  [1, 2, 3]                   │
│   'true'             ←→  True                         │
│   'null'             ←→  None                         │
│   '3.14'             ←→  3.14                         │
│                                                       │
│   json.dumps()  = Python → JSON string                │
│   json.loads()  = JSON string → Python                │
│   json.dump()   = Python → JSON file                  │
│   json.load()   = JSON file → Python                  │
└──────────────────────────────────────────────────────┘

2. Modul json Dasar

Python memiliki modul json built-in yang menyediakan semua fungsi yang dibutuhkan untuk bekerja dengan data JSON.

Python — json Module Basics
import json

# ===== json.dumps(): Python → JSON string =====
data = {
    "name": "Budi Santoso",
    "age": 25,
    "is_student": True,
    "courses": ["Python", "JavaScript", "SQL"],
    "address": None
}

json_string = json.dumps(data)
print(json_string)
# {"name": "Budi Santoso", "age": 25, "is_student": true,
#  "courses": ["Python", "JavaScript", "SQL"], "address": null}

print(type(json_string))  # <class 'str'>

# ===== json.loads(): JSON string → Python =====
json_text = '{"name": "Sari", "age": 30, "scores": [85, 90, 78]}'
python_data = json.loads(json_text)
print(python_data)
# {'name': 'Sari', 'age': 30, 'scores': [85, 90, 78]}

print(type(python_data))  # <class 'dict'>
print(python_data["name"])  # Sari

# ===== Konversi tipe data =====
# dict → JSON string
d = {"key": "value"}
s = json.dumps(d)

# list → JSON string
lst = [1, 2, 3, "hello", True, None]
s = json.dumps(lst)
print(s)  # [1, 2, 3, "hello", true, null]

# JSON string → dict
d = json.loads('{"a": 1, "b": 2}')

# JSON string → list
lst = json.loads('[1, 2, 3]')

# Nested structures
nested = json.dumps({
    "user": {
        "name": "Budi",
        "contacts": [
            {"type": "email", "value": "budi@email.com"},
            {"type": "phone", "value": "+62812345678"}
        ]
    }
})
print(json.dumps(json.loads(nested), indent=2))

3. Serialization (Python → JSON)

Serialization adalah proses mengkonversi objek Python ke format JSON string. Ada beberapa parameter yang bisa dikustomisasi.

Python — Serialization Options
import json

data = {
    "name": "Budi",
    "age": 25,
    "scores": [85, 90, 78],
    "active": True,
    "address": None
}

# ===== indent: Pretty print =====
print(json.dumps(data, indent=2))
# {
#   "name": "Budi",
#   "age": 25,
#   "scores": [85, 90, 78],
#   "active": true,
#   "address": null
# }

# Custom indent
print(json.dumps(data, indent=4))

# ===== separators: Custom delimiters =====
# Default: (', ', ': ')
# Compact:
print(json.dumps(data, separators=(',', ':')))
# {"name":"Budi","age":25,...}

# ===== sort_keys: Urutkan keys =====
print(json.dumps(data, indent=2, sort_keys=True))
# Keys diurutkan secara alfabetis

# ===== ensure_ascii: Non-ASCII characters =====
data_indo = {"kota": "Jakarta", "provinsi": "DKI Jakarta"}
# Default (True): escape non-ASCII
print(json.dumps(data_indo))
# {"kota": "Jakarta", "provinsi": "DKI Jakarta"}

# False: biarkan karakter asli
print(json.dumps(data_indo, ensure_ascii=False))
# {"kota": "Jakarta", "provinsi": "DKI Jakarta"}

# ===== default: Handle non-serializable objects =====
from datetime import datetime
from pathlib import Path

data_with_special = {
    "created": datetime.now(),
    "path": Path("/home/user"),
    "set_data": {1, 2, 3},
}

def custom_serializer(obj):
    """Handler untuk objek yang tidak bisa di-serialize."""
    if isinstance(obj, datetime):
        return obj.isoformat()
    elif isinstance(obj, Path):
        return str(obj)
    elif isinstance(obj, set):
        return list(obj)
    raise TypeError(f"Type {type(obj)} not serializable")

result = json.dumps(data_with_special, default=custom_serializer, indent=2)
print(result)

# ===== allow_nan: Handle NaN, Infinity =====
import math

data_special = {"value": float('nan'), "inf": float('inf')}
# Default: allow_nan=True → NaN dan Infinity diizinkan
print(json.dumps(data_special))  # {"value": NaN, "inf": Infinity}

# False: raise ValueError untuk NaN/Infinity
try:
    json.dumps(data_special, allow_nan=False)
except ValueError as e:
    print(f"Error: {e}")

4. Deserialization (JSON → Python)

Deserialization adalah proses mengkonversi JSON string kembali ke objek Python.

Python — Deserialization
import json

# ===== Basic deserialization =====
json_str = '{"name": "Sari", "age": 30, "hobbies": ["coding", "reading"]}'
data = json.loads(json_str)
print(f"Name: {data['name']}")
print(f"Hobbies: {data['hobbies']}")

# ===== object_hook: Custom object creation =====
class User:
    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email

    def __repr__(self):
        return f"User(name='{self.name}', age={self.age})"

def as_user(dct):
    """Convert dict to User object."""
    if 'name' in dct and 'age' in dct:
        return User(dct['name'], dct['age'], dct.get('email', ''))
    return dct

json_str = '{"name": "Budi", "age": 25, "email": "budi@mail.com"}'
user = json.loads(json_str, object_hook=as_user)
print(f"User: {user}")  # User(name='Budi', age=25)
print(f"Name: {user.name}")  # Budi

# ===== object_pairs_hook: Preserve key order =====
def ordered_pairs(pairs):
    """Convert to OrderedDict preserving order."""
    from collections import OrderedDict
    return OrderedDict(pairs)

json_str = '{"z": 1, "a": 2, "m": 3}'
ordered = json.loads(json_str, object_pairs_hook=ordered_pairs)
print(f"Ordered: {ordered}")  # OrderedDict([('z', 1), ('a', 2), ('m', 3)])

# ===== parse_float dan parse_int =====
from decimal import Decimal

json_str = '{"price": 19.99, "quantity": 100}'

# Parse float as Decimal (untuk presisi tinggi)
data = json.loads(json_str, parse_float=Decimal)
print(f"Price: {data['price']}")       # 19.99 (Decimal)
print(f"Type: {type(data['price'])}")   # <class 'decimal.Decimal'>

# Parse int as string (untuk bilangan sangat besar)
json_big = '{"huge_number": 123456789012345678901234567890}'
data = json.loads(json_big, parse_int=str)
print(f"Huge: {data['huge_number']}")  # String representation

# ===== Parse multiple JSON objects =====
json_lines = '{"name": "Budi"}\n{"name": "Sari"}\n{"name": "Andi"}'
objects = [json.loads(line) for line in json_lines.strip().split('\n')]
print(f"Found {len(objects)} objects")
for obj in objects:
    print(f"  {obj['name']}")

5. Custom JSON Encoder

Untuk menangani tipe data yang tidak bisa di-serialize secara default (datetime, Decimal, custom classes), kita membuat custom encoder.

Python — Custom Encoder
import json
from datetime import datetime, date, time
from decimal import Decimal
from pathlib import Path
from uuid import UUID
from dataclasses import dataclass, asdict
from typing import Any
from enum import Enum

class Status(Enum):
    ACTIVE = "active"
    INACTIVE = "inactive"
    PENDING = "pending"

class AdvancedJSONEncoder(json.JSONEncoder):
    """Custom encoder untuk menangani tipe data Python lanjutan."""

    def default(self, obj: Any) -> Any:
        # datetime dan date
        if isinstance(obj, datetime):
            return {
                "_type": "datetime",
                "value": obj.isoformat()
            }
        if isinstance(obj, date):
            return {
                "_type": "date",
                "value": obj.isoformat()
            }
        if isinstance(obj, time):
            return {
                "_type": "time",
                "value": obj.isoformat()
            }

        # Decimal
        if isinstance(obj, Decimal):
            return float(obj)

        # Path
        if isinstance(obj, Path):
            return str(obj)

        # UUID
        if isinstance(obj, UUID):
            return str(obj)

        # Set
        if isinstance(obj, set):
            return sorted(list(obj))

        # Enum
        if isinstance(obj, Enum):
            return obj.value

        # Bytes
        if isinstance(obj, bytes):
            return obj.decode('utf-8', errors='replace')

        # Fallback ke default encoder
        return super().default(obj)

# Menggunakan custom encoder
data = {
    "timestamp": datetime.now(),
    "date": date.today(),
    "price": Decimal("19.99"),
    "path": Path("/home/user/file.txt"),
    "status": Status.ACTIVE,
    "tags": {"python", "json", "tutorial"},
}

result = json.dumps(data, cls=AdvancedJSONEncoder, indent=2, ensure_ascii=False)
print(result)

# Dataclass support
@dataclass
class Product:
    id: int
    name: str
    price: float
    created: datetime
    tags: list

product = Product(
    id=1,
    name="Python Course",
    price=49.99,
    created=datetime.now(),
    tags=["python", "programming"]
)

# Dataclass → dict → JSON
product_json = json.dumps(asdict(product), cls=AdvancedJSONEncoder, indent=2)
print(product_json)

Alternative Encoder Approaches

Python — Encoder Alternatives
import json
from datetime import datetime
from decimal import Decimal
from typing import Any

# ===== Approach 1: default function (tanpa subclass) =====
def json_default(obj: Any) -> Any:
    if isinstance(obj, datetime):
        return obj.isoformat()
    if isinstance(obj, Decimal):
        return float(obj)
    if isinstance(obj, set):
        return list(obj)
    raise TypeError(f"Object of type {type(obj)} is not JSON serializable")

data = {"time": datetime.now(), "amount": Decimal("99.99")}
print(json.dumps(data, default=json_default))

# ===== Approach 2: __json__ protocol =====
class JSONSerializable:
    """Mixin yang menambahkan __json__ method."""

    def to_json(self) -> dict:
        """Override this method to define JSON representation."""
        raise NotImplementedError

    @classmethod
    def from_json(cls, data: dict):
        """Override this method to create from JSON."""
        raise NotImplementedError

class Employee(JSONSerializable):
    def __init__(self, name: str, department: str, salary: Decimal):
        self.name = name
        self.department = department
        self.salary = salary

    def to_json(self) -> dict:
        return {
            "name": self.name,
            "department": self.department,
            "salary": float(self.salary),
        }

    @classmethod
    def from_json(cls, data: dict):
        return cls(
            name=data["name"],
            department=data["department"],
            salary=Decimal(str(data["salary"])),
        )

def smart_default(obj):
    if hasattr(obj, 'to_json'):
        return obj.to_json()
    if isinstance(obj, datetime):
        return obj.isoformat()
    raise TypeError(f"Type {type(obj)} not serializable")

emp = Employee("Budi", "Engineering", Decimal("8500000"))
print(json.dumps(emp, default=smart_default, indent=2))

6. Custom JSON Decoder

Python — Custom Decoder
import json
from datetime import datetime, date
from decimal import Decimal

# ===== Custom decoder dengan object_hook =====
def decode_special_types(dct):
    """Decode tipe data khusus yang di-encode oleh AdvancedJSONEncoder."""
    if "_type" in dct:
        type_name = dct["_type"]
        value = dct["value"]

        if type_name == "datetime":
            return datetime.fromisoformat(value)
        elif type_name == "date":
            return date.fromisoformat(value)

    return dct

json_data = '{"timestamp": {"_type": "datetime", "value": "2026-06-26T10:30:00"}}'
result = json.loads(json_data, object_hook=decode_special_types)
print(f"Type: {type(result['timestamp'])}")  # datetime
print(f"Value: {result['timestamp']}")

# ===== Full roundtrip: Encode → Decode =====
# Encode
original = {
    "event": "Konferensi Python Indonesia",
    "date": datetime(2026, 8, 15, 9, 0),
    "price": Decimal("250000"),
    "attendees": {"Budi", "Sari", "Andi"},
}

from pathlib import Path
import json

class FullEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime):
            return {"__datetime__": True, "value": obj.isoformat()}
        if isinstance(obj, Decimal):
            return {"__decimal__": True, "value": str(obj)}
        if isinstance(obj, set):
            return {"__set__": True, "value": list(obj)}
        return super().default(obj)

def full_decoder(dct):
    if "__datetime__" in dct:
        return datetime.fromisoformat(dct["value"])
    if "__decimal__" in dct:
        return Decimal(dct["value"])
    if "__set__" in dct:
        return set(dct["value"])
    return dct

# Encode → Decode
encoded = json.dumps(original, cls=FullEncoder, ensure_ascii=False)
decoded = json.loads(encoded, object_hook=full_decoder)

print(f"Event: {decoded['event']}")
print(f"Date type: {type(decoded['date'])}")        # datetime
print(f"Price type: {type(decoded['price'])}")      # Decimal
print(f"Attendees type: {type(decoded['attendees'])}")  # set
print(f"Perfect roundtrip: {original == decoded}")  # True

7. Baca/Tulis JSON File

Python — JSON File I/O
import json
from pathlib import Path

# ===== json.dump(): Python → File =====
config = {
    "app_name": "MyApp",
    "version": "2.1.0",
    "database": {
        "host": "localhost",
        "port": 5432,
        "name": "myapp_db"
    },
    "features": {
        "dark_mode": True,
        "notifications": True,
        "max_upload_mb": 50
    }
}

# Tulis ke file
with open("config.json", "w", encoding="utf-8") as f:
    json.dump(config, f, indent=2, ensure_ascii=False)

# Dengan pathlib
config_path = Path("config.json")
config_path.write_text(json.dumps(config, indent=2), encoding="utf-8")

# ===== json.load(): File → Python =====
# Baca dari file
with open("config.json", "r", encoding="utf-8") as f:
    loaded_config = json.load(f)

print(f"App: {loaded_config['app_name']}")
print(f"DB Host: {loaded_config['database']['host']}")

# Dengan pathlib
config_data = json.loads(config_path.read_text(encoding="utf-8"))

# ===== Read JSON Lines (JSONL) =====
jsonl_path = Path("data.jsonl")

# Write JSONL
with open(jsonl_path, "w") as f:
    for i in range(5):
        record = {"id": i, "name": f"User-{i}", "score": i * 10}
        f.write(json.dumps(record) + "\n")

# Read JSONL
records = []
with open(jsonl_path, "r") as f:
    for line in f:
        line = line.strip()
        if line:
            records.append(json.loads(line))

print(f"Loaded {len(records)} records")

# ===== JSONL dengan pathlib =====
records = [
    json.loads(line)
    for line in jsonl_path.read_text().splitlines()
    if line.strip()
]

# ===== Append ke JSON file (manual) =====
# JSON file tidak mendukung append secara native
# Tapi bisa dilakukan dengan load-modify-save

def append_to_json_array(filepath: str, new_item: dict):
    """Menambahkan item ke array dalam JSON file."""
    path = Path(filepath)

    if path.exists():
        data = json.loads(path.read_text())
    else:
        data = []

    data.append(new_item)
    path.write_text(json.dumps(data, indent=2))

append_to_json_array("users.json", {"name": "Budi", "age": 25})
append_to_json_array("users.json", {"name": "Sari", "age": 30})

8. JSON dengan REST API

Python — JSON API
import json
import urllib.request
import urllib.parse
from typing import Dict, Any

# ===== GET Request dengan JSON response =====
def api_get(url: str) -> Dict[str, Any]:
    """Melakukan GET request dan parse JSON response."""
    req = urllib.request.Request(url)
    req.add_header("Accept", "application/json")

    with urllib.request.urlopen(req) as response:
        data = json.loads(response.read().decode("utf-8"))
        return data

# Contoh penggunaan
# users = api_get("https://jsonplaceholder.typicode.com/users")
# for user in users[:3]:
#     print(f"  {user['name']} — {user['email']}")

# ===== POST Request dengan JSON body =====
def api_post(url: str, data: dict) -> Dict[str, Any]:
    """Melakukan POST request dengan JSON body."""
    json_data = json.dumps(data).encode("utf-8")

    req = urllib.request.Request(
        url,
        data=json_data,
        method="POST"
    )
    req.add_header("Content-Type", "application/json")
    req.add_header("Accept", "application/json")

    with urllib.request.urlopen(req) as response:
        return json.loads(response.read().decode("utf-8"))

# Contoh penggunaan
# new_user = api_post("https://jsonplaceholder.typicode.com/users", {
#     "name": "Budi Santoso",
#     "email": "budi@example.com",
#     "phone": "+62812345678"
# })

# ===== JSON API Client =====
class JSONAPIClient:
    """Simple JSON API client."""

    def __init__(self, base_url: str, headers: dict = None):
        self.base_url = base_url.rstrip("/")
        self.headers = headers or {}
        self.headers.setdefault("Content-Type", "application/json")
        self.headers.setdefault("Accept", "application/json")

    def _request(self, method: str, endpoint: str, data: dict = None) -> dict:
        url = f"{self.base_url}/{endpoint.lstrip('/')}"

        body = json.dumps(data).encode("utf-8") if data else None
        req = urllib.request.Request(url, data=body, method=method)

        for key, value in self.headers.items():
            req.add_header(key, value)

        try:
            with urllib.request.urlopen(req) as response:
                content = response.read().decode("utf-8")
                if content:
                    return json.loads(content)
                return {}
        except urllib.error.HTTPError as e:
            error_body = e.read().decode("utf-8")
            raise Exception(f"API Error {e.code}: {error_body}")

    def get(self, endpoint: str) -> dict:
        return self._request("GET", endpoint)

    def post(self, endpoint: str, data: dict) -> dict:
        return self._request("POST", endpoint, data)

    def put(self, endpoint: str, data: dict) -> dict:
        return self._request("PUT", endpoint, data)

    def delete(self, endpoint: str) -> dict:
        return self._request("DELETE", endpoint)

# Menggunakan client
# api = JSONAPIClient("https://api.example.com")
# users = api.get("/users")
# new_user = api.post("/users", {"name": "Budi"})
# api.put("/users/1", {"name": "Budi Updated"})
# api.delete("/users/1")

9. Pretty Print dan Formatting

Python — JSON Formatting
import json

data = {
    "users": [
        {"id": 1, "name": "Budi", "scores": [85, 90, 78]},
        {"id": 2, "name": "Sari", "scores": [92, 88, 95]},
    ],
    "total": 2,
    "metadata": {"page": 1, "per_page": 10}
}

# ===== Compact (tanpa spasi) =====
compact = json.dumps(data, separators=(',', ':'))
print(f"Compact: {compact[:80]}...")
print(f"Size: {len(compact)} bytes")

# ===== Standard (default) =====
standard = json.dumps(data)
print(f"Standard: {standard[:80]}...")
print(f"Size: {len(standard)} bytes")

# ===== Pretty (indent 2) =====
pretty = json.dumps(data, indent=2)
print(f"Pretty:\n{pretty}")

# ===== Custom formatting =====
def format_json(data, indent=2, max_inline=60):
    """Format JSON dengan inline array kecuali jika terlalu panjang."""
    result = json.dumps(data, indent=indent, ensure_ascii=False)
    return result

# ===== Colored JSON di terminal =====
def colorize_json(json_str: str) -> str:
    """Menambahkan warna ANSI ke JSON string."""
    import re
    # Warna untuk tipe data berbeda
    json_str = re.sub(r'"([^"]+)":', r'\033[34m"\1"\033[0m:', json_str)  # Keys: blue
    json_str = re.sub(r': "([^"]*)"', r': \033[32m"\1"\033[0m', json_str)  # Strings: green
    json_str = re.sub(r': (\d+\.?\d*)', r': \033[33m\1\033[0m', json_str)  # Numbers: yellow
    json_str = re.sub(r': (true|false|null)', r': \033[35m\1\033[0m', json_str)  # Boolean/null: magenta
    return json_str

print(colorize_json(json.dumps(data, indent=2)))

# ===== JSON diff =====
def json_diff(obj1, obj2, path=""):
    """Mencari perbedaan antara dua JSON object."""
    diffs = []

    if isinstance(obj1, dict) and isinstance(obj2, dict):
        for key in set(list(obj1.keys()) + list(obj2.keys())):
            current = f"{path}.{key}" if path else key
            if key not in obj1:
                diffs.append(f"  + {current}: {obj2[key]}")
            elif key not in obj2:
                diffs.append(f"  - {current}: {obj1[key]}")
            else:
                diffs.extend(json_diff(obj1[key], obj2[key], current))
    elif isinstance(obj1, list) and isinstance(obj2, list):
        for i in range(max(len(obj1), len(obj2))):
            current = f"{path}[{i}]"
            if i >= len(obj1):
                diffs.append(f"  + {current}: {obj2[i]}")
            elif i >= len(obj2):
                diffs.append(f"  - {current}: {obj1[i]}")
            else:
                diffs.extend(json_diff(obj1[i], obj2[i], current))
    elif obj1 != obj2:
        diffs.append(f"  ~ {path}: {obj1} → {obj2}")

    return diffs

old_data = {"name": "Budi", "age": 25, "city": "Jakarta"}
new_data = {"name": "Budi", "age": 26, "email": "budi@mail.com"}
print("\nJSON Diff:")
for diff in json_diff(old_data, new_data):
    print(diff)

10. Error Handling

Python — JSON Error Handling
import json

# ===== JSONDecodeError =====
invalid_json_strings = [
    '{"name": "Budi",}',         # Trailing comma
    "{'name': 'Budi'}",          # Single quotes
    '{"name": undefined}',        # undefined bukan JSON
    '{name: "Budi"}',            # Key tanpa quotes
    '{"value": 123abc}',         # Invalid number
    '',                          # Empty string
    'Just plain text',           # Bukan JSON
]

for s in invalid_json_strings:
    try:
        json.loads(s)
        print(f"  ✅ Valid: {s[:50]}")
    except json.JSONDecodeError as e:
        print(f"  ❌ Error: {str(e)[:60]} | Input: {s[:40]}")

# ===== Safe JSON parsing =====
def safe_json_loads(text: str, default=None):
    """Parse JSON dengan aman, mengembalikan default jika gagal."""
    try:
        return json.loads(text)
    except (json.JSONDecodeError, TypeError):
        return default

# Test
result = safe_json_loads('{"valid": true}', default={})
print(f"Valid: {result}")

result = safe_json_loads('invalid json', default={})
print(f"Invalid: {result}")

# ===== Handle serialization errors =====
def safe_json_dumps(data, default=None, **kwargs):
    """Serialize ke JSON dengan aman."""
    try:
        return json.dumps(data, **kwargs)
    except (TypeError, ValueError) as e:
        if default is not None:
            return json.dumps(default, **kwargs)
        raise

# ===== Validate JSON structure =====
def validate_json_structure(data: dict, schema: dict) -> list:
    """Validasi sederhana JSON berdasarkan schema dict."""
    errors = []

    for key, expected_type in schema.items():
        if key not in data:
            errors.append(f"Missing key: '{key}'")
        elif not isinstance(data[key], expected_type):
            errors.append(
                f"Wrong type for '{key}': "
                f"expected {expected_type.__name__}, "
                f"got {type(data[key]).__name__}"
            )

    return errors

# Test validation
schema = {
    "name": str,
    "age": int,
    "email": str,
    "scores": list,
}

test_data = {"name": "Budi", "age": "25", "scores": [90, 85]}
errors = validate_json_structure(test_data, schema)
if errors:
    print(f"Validation errors:")
    for err in errors:
        print(f"  ❌ {err}")

11. Studi Kasus

Configuration Manager

Python — Config Manager
import json
from pathlib import Path
from typing import Any, Optional
from copy import deepcopy

class ConfigManager:
    """JSON-based configuration manager dengan merge dan env support."""

    def __init__(self, config_path: str, defaults: dict = None):
        self.config_path = Path(config_path)
        self.defaults = defaults or {}
        self.config = deepcopy(self.defaults)
        self._load()

    def _load(self):
        """Load config dari file, merge dengan defaults."""
        if self.config_path.exists():
            try:
                file_config = json.loads(self.config_path.read_text())
                self._deep_merge(self.config, file_config)
            except json.JSONDecodeError as e:
                print(f"⚠️ Config parse error: {e}")

    def _deep_merge(self, base: dict, override: dict):
        """Deep merge dua dictionary."""
        for key, value in override.items():
            if key in base and isinstance(base[key], dict) and isinstance(value, dict):
                self._deep_merge(base[key], value)
            else:
                base[key] = value

    def get(self, key_path: str, default: Any = None) -> Any:
        """Akses nested config dengan dot notation."""
        keys = key_path.split(".")
        current = self.config
        for key in keys:
            if isinstance(current, dict) and key in current:
                current = current[key]
            else:
                return default
        return current

    def set(self, key_path: str, value: Any):
        """Set nested config dengan dot notation."""
        keys = key_path.split(".")
        current = self.config
        for key in keys[:-1]:
            if key not in current:
                current[key] = {}
            current = current[key]
        current[keys[-1]] = value

    def save(self):
        """Simpan config ke file."""
        self.config_path.parent.mkdir(parents=True, exist_ok=True)
        self.config_path.write_text(
            json.dumps(self.config, indent=2, ensure_ascii=False),
            encoding="utf-8"
        )

    def __repr__(self):
        return json.dumps(self.config, indent=2, ensure_ascii=False)

# Menggunakan
defaults = {
    "app": {
        "name": "MyApp",
        "version": "1.0.0",
        "debug": False,
    },
    "database": {
        "host": "localhost",
        "port": 5432,
    },
    "logging": {
        "level": "INFO",
        "file": "app.log",
    }
}

config = ConfigManager("app_config.json", defaults)
print(f"App: {config.get('app.name')}")
print(f"DB Host: {config.get('database.host')}")
print(f"Debug: {config.get('app.debug')}")

config.set("app.debug", True)
config.set("database.host", "db.production.com")
config.save()
print(f"Config saved!")

JSON Data Pipeline

Python — Data Pipeline
import json
from pathlib import Path
from datetime import datetime
from typing import List, Dict, Any

class JSONDataPipeline:
    """Pipeline untuk memproses JSON data."""

    def __init__(self):
        self.data: List[Dict] = []
        self.transforms: List[callable] = []
        self.filters: List[callable] = []

    def load_json(self, filepath: str) -> 'JSONDataPipeline':
        """Load data dari JSON file."""
        content = Path(filepath).read_text()
        loaded = json.loads(content)
        if isinstance(loaded, list):
            self.data.extend(loaded)
        else:
            self.data.append(loaded)
        return self

    def load_jsonl(self, filepath: str) -> 'JSONDataPipeline':
        """Load data dari JSON Lines file."""
        for line in Path(filepath).read_text().splitlines():
            if line.strip():
                self.data.append(json.loads(line))
        return self

    def filter(self, func: callable) -> 'JSONDataPipeline':
        """Tambahkan filter function."""
        self.filters.append(func)
        return self

    def transform(self, func: callable) -> 'JSONDataPipeline':
        """Tambahkan transform function."""
        self.transforms.append(func)
        return self

    def process(self) -> List[Dict]:
        """Jalankan pipeline."""
        result = self.data

        # Apply filters
        for f in self.filters:
            result = [item for item in result if f(item)]

        # Apply transforms
        for t in self.transforms:
            result = [t(item) for item in result]

        return result

    def save_json(self, data: List[Dict], filepath: str, indent: int = 2):
        """Simpan hasil ke JSON file."""
        Path(filepath).write_text(
            json.dumps(data, indent=indent, ensure_ascii=False),
            encoding="utf-8"
        )
        print(f"  💾 Saved {len(data)} records to {filepath}")

    def save_jsonl(self, data: List[Dict], filepath: str):
        """Simpan hasil ke JSON Lines file."""
        lines = [json.dumps(item, ensure_ascii=False) for item in data]
        Path(filepath).write_text("\n".join(lines) + "\n")
        print(f"  💾 Saved {len(data)} records to {filepath}")

# Menggunakan pipeline
# pipeline = JSONDataPipeline()
# result = (
#     pipeline
#     .load_json("users.json")
#     .filter(lambda u: u.get("age", 0) >= 18)
#     .filter(lambda u: u.get("active", True))
#     .transform(lambda u: {**u, "processed_at": datetime.now().isoformat()})
#     .transform(lambda u: {k: v for k, v in u.items() if k != "password"})
#     .process()
# )
# pipeline.save_json(result, "adult_users.json")

12. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut:

Pertanyaan 1: Fungsi apa yang digunakan untuk mengkonversi JSON string ke Python object?

a) json.dumps()
b) json.loads()
c) json.decode()
d) json.parse()

Pertanyaan 2: Apa perbedaan antara json.dumps() dan json.dump()?

a) Tidak ada perbedaan
b) dumps() return string, dump() tulis ke file
c) dumps() untuk dict, dump() untuk list
d) dumps() lebih cepat dari dump()

Pertanyaan 3: Tipe data Python apa yang direpresentasikan sebagai null dalam JSON?

a) False
b) 0
c) None
d) ""

Pertanyaan 4: Parameter apa yang digunakan untuk membuat JSON output lebih rapi?

a) format=True
b) pretty=True
c) indent=N
d) beautify=True

Pertanyaan 5: Bagaimana cara menangani tipe data yang tidak bisa di-serialize (misal: datetime)?

a) Gunakan parameter allow_special=True
b) Konversi manual ke string sebelum dumps
c) Gunakan parameter default=handler_func
d) Gunakan json.dumps(obj, safe=False)