Python e JSON sono una coppia naturale. Che tu stia costruendo una REST API con FastAPI o Django, elaborando pipeline di dati o semplicemente leggendo un file di configurazione, lavorerai con JSON continuamente. La buona notizia: la libreria standard di Python ha tutto ciò di cui hai bisogno nel modulo json. Nessun pip install richiesto.

Le Quattro Funzioni che Usi Davvero

Il modulo json ti offre quattro funzioni per il lavoro quotidiano:

  • json.loads(str) — analizza una stringa JSON in un oggetto Python
  • json.dumps(obj) — converte un oggetto Python in una stringa JSON
  • json.load(file) — analizza JSON direttamente da un oggetto file
  • json.dump(obj, file) — scrive un oggetto Python come JSON su un file

La s in loads / dumps sta per stringa. Quelle senza la s lavorano con oggetti file. Facile da ricordare una volta che conosci la regola.

json.loads() — Analisi di una Stringa JSON

python
import json

json_string = '{"name": "Alice", "age": 30, "active": true, "score": 98.5}'

user = json.loads(json_string)

print(user["name"])    # Alice
print(user["age"])     # 30
print(user["active"])  # True
print(type(user))      # <class 'dict'>

Nota la mappatura dei tipi: JSON true diventa Python True, JSON false diventa Python False, JSON null diventa Python None. Gli oggetti JSON diventano dict Python, gli array JSON diventano list Python.

json.dumps() — Serializzazione in una Stringa JSON

python
import json

user = {
    "name": "Bob",
    "age": 25,
    "roles": ["admin", "editor"],
    "active": True,
    "extra": None
}

# Compatto (ottimo per la trasmissione in rete)
compact = json.dumps(user)
print(compact)
# {"name": "Bob", "age": 25, "roles": ["admin", "editor"], "active": true, "extra": null}

# Formattato (ottimo per log e ispezione umana)
pretty = json.dumps(user, indent=2)
print(pretty)
# {
#   "name": "Bob",
#   "age": 25,
#   "roles": [
#     "admin",
#     "editor"
#   ],
#   "active": true,
#   "extra": null
# }

Nota la mappatura inversa dei tipi: Python True → JSON true, Python None → JSON null. Python gestisce questo automaticamente.

Lettura di JSON da un File

Questo è probabilmente il caso d'uso più comune — leggere un file di configurazione o dati all'avvio:

python
import json

# Leggi e analizza in un solo passaggio
with open("config.json", "r", encoding="utf-8") as f:
    config = json.load(f)

print(config["database"]["host"])  # localhost
print(config["database"]["port"])  # 5432

Specifica sempre encoding="utf-8" quando apri file JSON. JSON è specificato come UTF-8 dalla RFC 8259, e omettere questa specifica può causare problemi su Windows dove la codifica predefinita è a volte cp1252.

Scrittura di JSON su un File

python
import json

results = {
    "timestamp": "2024-01-15T09:30:00Z",
    "total": 1523,
    "processed": 1521,
    "failed": 2,
    "errors": [
        {"id": 42, "reason": "missing field"},
        {"id": 99, "reason": "invalid format"}
    ]
}

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

print("Results saved to results.json")

Gestione Corretta degli Errori

json.loads() solleva json.JSONDecodeError (una sottoclasse di ValueError) quando l'input non è JSON valido. Gestiscila sempre quando analizzi dati che non controlli:

python
import json

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

data = safe_parse('{"name": "Alice"}')   # works fine
bad  = safe_parse('not json at all')     # prints error, returns None
also_bad = safe_parse('{"key": }')       # prints error with position info

JSONDecodeError ti fornisce la riga e la colonna esatte dove l'analisi è fallita, il che è utile quando si esegue il debug di file JSON di grandi dimensioni.

Opzioni Utili di dumps()

python
import json

data = {
    "z_key": 1,
    "a_key": 2,
    "price": 9.999999999
}

# Ordina le chiavi alfabeticamente (ottimo per output riproducibili / diff)
print(json.dumps(data, sort_keys=True, indent=2))
# {
#   "a_key": 2,
#   "price": 9.999999999,
#   "z_key": 1
# }

# Assicura che i caratteri non-ASCII vengano preservati (predefinito: escape in \uXXXX)
data2 = {"city": "Münich", "greeting": "こんにちは"}
print(json.dumps(data2, ensure_ascii=False))
# {"city": "Münich", "greeting": "こんにちは"}

# Con ensure_ascii=True (predefinito):
print(json.dumps(data2))
# {"city": "M\u00fcnich", "greeting": "\u3053\u3093\u306b\u3061\u306f"}

ensure_ascii=False è qualcosa che aggiungo sempre quando scrivo file JSON che contengono testo non-ASCII. La versione con escape è tecnicamente JSON valido ma molto più difficile da leggere in un editor di testo.

Serializzazione di Oggetti Personalizzati

Per impostazione predefinita, json.dumps() non può serializzare istanze di classi personalizzate o oggetti datetime. Hai due opzioni: creare una sottoclasse di json.JSONEncoder, oppure convertire prima in un dict:

python
import json
from datetime import datetime, date

# Opzione 1: classe encoder personalizzata
class AppEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (datetime, date)):
            return obj.isoformat()
        return super().default(obj)

data = {"name": "Alice", "created_at": datetime(2024, 1, 15, 9, 30)}
print(json.dumps(data, cls=AppEncoder, indent=2))
# {
#   "name": "Alice",
#   "created_at": "2024-01-15T09:30:00"
# }

# Opzione 2: parametro default= (più semplice per conversioni occasionali)
print(json.dumps(data, default=str, indent=2))  # converte tutto lo sconosciuto in str

Un Pattern Pratico: Caricamento di File di Configurazione

Ecco un pattern reale che uso in quasi ogni progetto Python — un caricatore di configurazione che legge un file JSON con valori predefiniti ragionevoli:

python
import json
import os
from pathlib import Path

DEFAULTS = {
    "database": {"host": "localhost", "port": 5432},
    "debug": False,
    "log_level": "INFO"
}

def load_config(path="config.json"):
    config = DEFAULTS.copy()

    config_path = Path(path)
    if config_path.exists():
        with open(config_path, "r", encoding="utf-8") as f:
            try:
                user_config = json.load(f)
                # Merge profondo: le impostazioni utente sovrascrivono i valori predefiniti
                for key, value in user_config.items():
                    if isinstance(value, dict) and key in config:
                        config[key].update(value)
                    else:
                        config[key] = value
            except json.JSONDecodeError as e:
                print(f"Warning: config.json is invalid ({e.msg}), using defaults")

    return config

config = load_config()
print(config["database"]["host"])  # localhost (or overridden value)

Conclusioni

Il modulo json di Python copre tutto ciò di cui hai bisogno senza dipendenze esterne. Le regole fondamentali: usa loads()/dumps() per le stringhe, load()/dump() per i file, gestisci sempre JSONDecodeError quando analizzi dati esterni, e aggiungi ensure_ascii=False quando i tuoi dati contengono caratteri non latini. Per il debug dei dati JSON, il Formattatore JSON e il Validatore JSON possono farti risparmiare molto tempo.