JSON #
JSON (JavaScript Object Notation) is the most widely used data-exchange format in modern software development — from API responses, configuration files, to inter-service communication. Python provides the complete built-in json module for reading, writing, and manipulating JSON data without installing any extra library. Understanding how this module works deeply — including type mapping, error handling, and custom object serialization patterns — will save you from hard-to-trace bugs in production environments.
Python ↔ JSON Type Mapping #
Before getting started, it’s important to understand how Python and JSON map their data types to each other. Unexpected conversions are often a source of bugs that aren’t immediately visible.
In general, the interaction between Python data types and JSON data involves two main processes: Serialization (converting Python objects into JSON format) and Deserialization (reading JSON format back into Python objects). The workflow of both processes can be illustrated as follows:
flowchart LR
subgraph Python ["Python Environment"]
Obj["Python Object (dict, list, str, int, True, None)"]
end
subgraph Operations ["json Module"]
direction TB
dumps["json.dumps() / json.dump()"]
loads["json.loads() / json.load()"]
end
subgraph JSON ["JSON Data"]
JSONStr["JSON String / File (object, array, string, number, true, null)"]
end
Obj -->|"Serialization (dumps / dump)"| dumps
dumps --> JSONStr
JSONStr -->|"Deserialization (loads / load)"| loads
loads --> ObjPython → JSON:
| Python Data Type | JSON Data Type |
|---|---|
dict | object {} |
list, tuple | array [] |
str | string "" |
int, float | number |
True | true |
False | false |
None | null |
JSON → Python:
| JSON Data Type | Python Data Type |
|---|---|
object {} | dict |
array [] | list |
string "" | str |
| number (int) | int |
| number (float) | float |
| true | True |
| false | False |
| null | None |
Note: a Pythontupleis converted into a JSONarray, but when read back it becomes a Pythonlist— not atuple. If type ordering matters, don’t rely on a JSON round-trip to preserve tuple types.
Parsing JSON #
The json module provides two functions for reading JSON data: loads() for strings and load() for files. The difference is small but often mixed up.
import json
# json.loads() -- from a JSON string
json_string = '{"nama": "Budi", "usia": 28, "aktif": true}'
data = json.loads(json_string)
print(data["nama"]) # Budi
print(data["aktif"]) # True (not "true" -- already a Python bool)
print(type(data)) # <class 'dict'>
# json.load() -- from a file
with open("data.json", "r", encoding="utf-8") as f:
data = json.load(f)
# ANTI-PATTERN: reading a JSON file without explicit encoding
with open("data.json", "r") as f: # ✗ -- fails on Windows with non-ASCII characters
data = json.load(f)
# CORRECT: always include encoding="utf-8"
with open("data.json", "r", encoding="utf-8") as f: # ✓
data = json.load(f)
Serializing to JSON #
Two functions for producing JSON: dumps() produces a string, dump() writes directly to a file.
import json
data = {
"nama": "Budi",
"usia": 28,
"hobi": ["membaca", "coding"],
"aktif": True,
"alamat": None
}
# To a JSON string (compact)
json_compact = json.dumps(data)
print(json_compact)
# {"nama": "Budi", "usia": 28, "hobi": ["membaca", "coding"], "aktif": true, "alamat": null}
# To a JSON string (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
# }
# Directly to a file
with open("output.json", "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
ensure_ascii=False— this parameter is important if your data contains non-ASCII characters like accented letters, Arabic characters, or Indonesian characters. Without it, those characters get escaped into\uXXXX, which is still valid JSON but hard to read.
Error Handling #
Parsing JSON from external sources (APIs, user-uploaded files, form input) should always be wrapped in error handling. Invalid data should be handled explicitly, not left to crash.
import json
# ANTI-PATTERN: parsing without error handling
data = json.loads(user_input) # ✗ -- crashes if the input isn't valid JSON
# CORRECT: handle JSONDecodeError explicitly
def parse_json_safe(raw: str) -> dict | None:
try:
return json.loads(raw)
except json.JSONDecodeError as e:
print(f"Invalid JSON: {e.msg} (line {e.lineno}, column {e.colno})")
return None
# Usage example
result = parse_json_safe('{"nama": "Budi"') # incomplete JSON
# Output: Invalid JSON: Expecting ',' delimiter (line 1, column 16)
# Returns: None
result = parse_json_safe('{"nama": "Budi"}')
# Returns: {"nama": "Budi"}
# Validate before processing
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 -- keys without quotes aren't valid JSON
print(is_valid_json('null')) # True -- null is valid JSON
print(is_valid_json('')) # False
Serializing Custom Objects #
By default, json.dumps() can only handle Python’s built-in types. Custom class objects, datetime, Decimal, and set will cause a TypeError. There are two ways to deal with this.
Using the default Parameter
#
import json
from datetime import datetime, date
from decimal import Decimal
# ANTI-PATTERN: serializing an unsupported object directly
from datetime import datetime
data = {"waktu": datetime.now()}
json.dumps(data) # ✗ -- TypeError: Object of type datetime is not JSON serializable
# CORRECT: use a default function
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"Type {type(obj).__name__} can't be serialized")
data = {
"nama": "Budi",
"dibuat": datetime(2024, 3, 15, 10, 30),
"harga": Decimal("99999.99"),
"tag": {"python", "backend"}
}
result = json.dumps(data, default=json_serializer, indent=2, ensure_ascii=False)
print(result)
# {
# "nama": "Budi",
# "dibuat": "2024-03-15T10:30:00",
# "harga": 99999.99,
# "tag": ["python", "backend"]
# }
Using a Custom JSONEncoder #
For larger projects, subclassing JSONEncoder is cleaner:
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 for consistent output
return super().default(obj)
data = {
"transaksi_id": "TRX-001",
"waktu": datetime(2024, 3, 15, 10, 30),
"total": Decimal("150000.00"),
"kategori": {"makanan", "minuman"}
}
result = json.dumps(data, cls=AppJSONEncoder, indent=2)
print(result)
Deserializing into Custom Objects #
object_hook lets you convert every dict parsed from JSON into a custom Python object. This is useful for directly getting method-rich objects instead of raw dicts.
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):
# Only convert if all the required fields are present
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
Serializing Dataclasses #
Since Python 3.7, dataclass is the modern way to define data classes. For JSON serialization, use 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"]
)
# Convert to a dict first, then to 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"]
# }
Common Patterns in Real Applications #
Reading Configuration from a JSON File #
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"Config file not found: {config_path}")
with open(path, "r", encoding="utf-8") as f:
try:
config = json.load(f)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid config.json format: {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
Storing and Reading Cache Data #
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() # delete the corrupted cache
return None
Processing JSON API Responses #
import json
# Simulated API response
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: accessing nested dicts without a guard
nama = response["data"]["users"][0]["nama"] # ✗ -- KeyError if the structure changes
# CORRECT: use .get() for safe access
users = response.get("data", {}).get("users", [])
active_users = [u for u in users if u.get("aktif", False)]
for user in active_users:
print(f"ID: {user.get('id')}, Name: {user.get('nama')}")
# Output: ID: 1, Name: Budi
When NOT to Use the Built-in json Module
#
Keep using the built-in json module when:
✓ Working with standard JSON structures
✓ No extreme performance requirements
✓ You want zero extra dependencies
✓ Simple data serialization/deserialization
Consider alternative libraries when:
✗ You need JSON schema validation (use jsonschema)
✗ You need high performance parsing of large JSON (use orjson or ujson)
✗ Working with Pydantic models (use model.model_dump_json())
✗ You need automatic class serialization without boilerplate (use cattrs or marshmallow)
Summary #
json.loads()vsjson.load()—loads()from a string,load()from a file object. Don’t mix them up.json.dumps()vsjson.dump()—dumps()produces a string,dump()writes directly to a file.- Always
encoding="utf-8"— include it when opening JSON files to avoid non-ASCII character issues across OSes.ensure_ascii=False— use it so Indonesian/non-Latin characters aren’t escaped into\uXXXX.JSONDecodeError— always wrap parsing from external sources in try/except to handle invalid input.- Custom encoders — use the
defaultparameter or aJSONEncodersubclass to serializedatetime,Decimal,set, or custom objects.object_hook— use it to automatically convert parsed dicts into custom Python objects.dataclasses.asdict()— the cleanest way to serialize dataclasses to JSON..get()for nested access — protect dict access from external JSON with.get()to avoidKeyErrorif the structure changes.