Python's built-in json module makes working with JSON simple and intuitive. Whether you are consuming a REST API, storing configuration, or processing data files, Python's JSON tools handle the job with minimal code. This guide covers every common JSON operation in Python - from basic parsing to custom serialisation, error handling, and working with APIs.

The json Module: Four Core Functions

The json module has four primary functions:

  • json.loads(string) - parse a JSON string into a Python object
  • json.dumps(obj) - serialise a Python object to a JSON string
  • json.load(file) - parse JSON from a file object
  • json.dump(obj, file) - serialise Python object and write to a file

The "s" in loads and dumps stands for "string" - a helpful way to remember the distinction.

Parsing JSON Strings

import json

# Parse a simple JSON string
json_string = '{"name": "Alice", "age": 30, "active": true}'
data = json.loads(json_string)

print(data["name"])    # Alice
print(data["age"])     # 30 (integer, not string!)
print(data["active"])  # True (Python bool)
print(type(data))      # <class 'dict'>

# Parse an array
array_string = '[1, 2, 3, "four", null, true]'
items = json.loads(array_string)
print(items[3])  # four
print(items[4])  # None (Python's null equivalent)
print(items[5])  # True

JSON types map to Python types as follows:

JSON TypePython TypeExample
objectdict{"a": 1}{'a': 1}
arraylist[1,2][1, 2]
stringstr"hello"'hello'
number (int)int4242
number (float)float3.143.14
true/falseTrue/FalsetrueTrue
nullNonenullNone

Reading and Writing JSON Files

import json

# Read JSON from a file
with open("config.json", "r", encoding="utf-8") as f:
    config = json.load(f)

print(config["database"]["host"])

# Write Python dict to a JSON file
data = {
    "version": "1.0",
    "database": {
        "host": "localhost",
        "port": 5432,
        "name": "myapp"
    },
    "features": ["auth", "logging", "cache"]
}

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

# The file will contain formatted JSON:
# {
#   "version": "1.0",
#   "database": { ... },
#   ...
# }

Pretty Printing JSON

import json

data = {"name": "Alice", "scores": [95, 87, 92], "address": {"city": "London"}}

# Compact (default)
print(json.dumps(data))
# {"name": "Alice", "scores": [95, 87, 92], "address": {"city": "London"}}

# Indented - 2 or 4 spaces (your preference)
print(json.dumps(data, indent=2))
# {
#   "name": "Alice",
#   "scores": [95, 87, 92],
#   "address": {
#     "city": "London"
#   }
# }

# Sorted keys - useful for reproducible output and diffing
print(json.dumps(data, indent=2, sort_keys=True))

# Print directly from a file to inspect it
with open("data.json") as f:
    print(json.dumps(json.load(f), indent=2))

Serialising Python Objects

Not all Python objects can be serialised to JSON by default. The following types cause a TypeError:

import json
from datetime import datetime, date
from decimal import Decimal
import uuid

data = {
    "created_at": datetime.now(),  # TypeError!
    "price": Decimal("9.99"),      # TypeError!
    "id": uuid.uuid4()             # TypeError!
}

To handle these types, provide a custom default function or a subclass of json.JSONEncoder:

import json
from datetime import datetime, date
from decimal import Decimal
import uuid

def default_serialiser(obj):
    if isinstance(obj, (datetime, date)):
        return obj.isoformat()
    if isinstance(obj, Decimal):
        return float(obj)
    if isinstance(obj, uuid.UUID):
        return str(obj)
    raise TypeError(f"Object of type {type(obj)} is not JSON serializable")

data = {
    "created_at": datetime.now(),
    "price": Decimal("9.99"),
    "id": uuid.uuid4()
}

# Pass the custom function as the 'default' argument
json_string = json.dumps(data, default=default_serialiser, indent=2)
print(json_string)

Using JSON with the Requests Library

The requests library makes consuming JSON APIs simple:

import requests

# GET request - parse JSON response
response = requests.get("https://api.example.com/users/1")
response.raise_for_status()  # raises HTTPError for 4xx/5xx status codes
user = response.json()       # automatically parses JSON response
print(user["name"])

# POST request with JSON body
new_user = {
    "name": "Bob Smith",
    "email": "bob@example.com",
    "role": "editor"
}

response = requests.post(
    "https://api.example.com/users",
    json=new_user,  # sets Content-Type: application/json and serialises automatically
    headers={"Authorization": "Bearer your-token"}
)
response.raise_for_status()
created_user = response.json()
print(f"Created user with ID: {created_user['id']}")

# PUT request to update a resource
updates = {"name": "Robert Smith", "active": True}
response = requests.put(
    f"https://api.example.com/users/{created_user['id']}",
    json=updates,
    headers={"Authorization": "Bearer your-token"}
)
response.raise_for_status()

Error Handling

import json

# Handle malformed JSON
def safe_parse(json_string):
    try:
        return json.loads(json_string)
    except json.JSONDecodeError as e:
        print(f"Invalid JSON at line {e.lineno}, column {e.colno}: {e.msg}")
        return None

# Handle missing keys safely
data = json.loads('{"user": {"name": "Alice"}}')

# Bad - raises KeyError if email is missing
# email = data["user"]["email"]

# Good - .get() returns None (or a default) for missing keys
email = data.get("user", {}).get("email", "no email provided")
print(email)  # no email provided

Processing Large JSON Files

For very large JSON files (gigabytes), loading everything into memory at once is impractical. Use the ijson library for streaming JSON parsing:

import ijson

# Stream through a large array of objects
with open("large_data.json", "rb") as f:
    for item in ijson.items(f, "item"):
        # process one item at a time without loading the whole file
        print(item["name"])

# Stream nested objects
with open("large_data.json", "rb") as f:
    for key, value in ijson.kvitems(f, "users.item"):
        print(key, value)

Converting JSON to Python Dataclasses

For typed, structured access to JSON data, Python dataclasses (Python 3.7+) combined with the dacite or marshmallow library provide an excellent pattern:

from dataclasses import dataclass
from typing import Optional, List
import json
import dacite

@dataclass
class Address:
    street: str
    city: str
    zip: Optional[str] = None

@dataclass
class User:
    id: int
    name: str
    email: str
    address: Address
    tags: List[str]

json_string = '''
{
  "id": 1,
  "name": "Alice",
  "email": "alice@example.com",
  "address": {"street": "123 Main St", "city": "New York"},
  "tags": ["admin", "developer"]
}
'''

data = json.loads(json_string)
user = dacite.from_dict(data_class=User, data=data)
print(user.name)           # Alice
print(user.address.city)   # New York
print(user.tags[0])        # admin

Explore Any JSON Structure Before Processing It

Before writing Python code to process a JSON response, use JSON Keyper to extract all key paths. This gives you a complete map of the data structure so you know exactly which fields to access.

Open JSON Keyper