Python i JSON to naturalne połączenie. Niezależnie od tego, czy budujesz REST API z FastAPI czy Django, przetwarzasz potoki danych, czy po prostu wczytujesz plik konfiguracyjny — z JSON-em będziesz pracować nieustannie. Dobra wiadomość: biblioteka standardowa Pythona ma wszystko, czego potrzebujesz, w module json. Bez pip install.

Cztery funkcje, których naprawdę używasz

Moduł json udostępnia cztery funkcje do codziennej pracy:

  • json.loads(str) — parsuje łańcuch JSON do obiektu Pythona
  • json.dumps(obj) — konwertuje obiekt Pythona do łańcucha JSON
  • json.load(file) — parsuje JSON bezpośrednio z obiektu pliku
  • json.dump(obj, file) — zapisuje obiekt Pythona jako JSON do pliku

Litera s w loads / dumps oznacza string (łańcuch). Wersje bez s pracują z obiektami plików. Łatwe do zapamiętania, gdy znasz tę zasadę.

json.loads() — Parsowanie łańcucha 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'>

Zwróć uwagę na mapowanie typów: JSON true staje się Pythonowym True, JSON false staje się False, JSON null staje się None. Obiekty JSON stają się słownikami dict, tablice JSON stają się listami list.

json.dumps() — Serializacja do łańcucha JSON

python
import json

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

# Kompaktowy (dobry do transmisji sieciowej)
compact = json.dumps(user)
print(compact)
# {"name": "Bob", "age": 25, "roles": ["admin", "editor"], "active": true, "extra": null}

# Sformatowany (dobry do logów i czytelności)
pretty = json.dumps(user, indent=2)
print(pretty)
# {
#   "name": "Bob",
#   "age": 25,
#   "roles": [
#     "admin",
#     "editor"
#   ],
#   "active": true,
#   "extra": null
# }

Zauważ odwrotne mapowanie typów: Python True → JSON true, Python None → JSON null. Python obsługuje to automatycznie.

Wczytywanie JSON z pliku

To prawdopodobnie najczęstszy przypadek użycia — wczytanie pliku konfiguracyjnego lub pliku z danymi podczas uruchamiania:

python
import json

# Odczyt i parsowanie w jednym kroku
with open("config.json", "r", encoding="utf-8") as f:
    config = json.load(f)

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

Zawsze podawaj encoding="utf-8" przy otwieraniu plików JSON. JSON jest zdefiniowany jako UTF-8 przez RFC 8259, a jego pominięcie może powodować problemy w systemie Windows, gdzie domyślne kodowanie to czasem cp1252.

Zapisywanie JSON do pliku

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")

Prawidłowa obsługa błędów

json.loads() zgłasza wyjątek json.JSONDecodeError (podklasa ValueError), gdy dane wejściowe nie są poprawnym JSON-em. Zawsze obsługuj go podczas parsowania danych, nad którymi nie masz kontroli:

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"}')   # działa poprawnie
bad  = safe_parse('not json at all')     # wypisuje błąd, zwraca None
also_bad = safe_parse('{"key": }')       # wypisuje błąd z informacją o pozycji

JSONDecodeError podaje dokładną linię i kolumnę, w której parsowanie się nie powiodło — przydatne przy debugowaniu dużych plików JSON.

Przydatne opcje dumps()

python
import json

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

# Sortowanie kluczy alfabetycznie (świetne dla powtarzalnego wyjścia / diffów)
print(json.dumps(data, sort_keys=True, indent=2))
# {
#   "a_key": 2,
#   "price": 9.999999999,
#   "z_key": 1
# }

# Zachowanie znaków spoza ASCII (domyślnie: escape do \uXXXX)
data2 = {"city": "Münich", "greeting": "こんにちは"}
print(json.dumps(data2, ensure_ascii=False))
# {"city": "Münich", "greeting": "こんにちは"}

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

ensure_ascii=False to opcja, którą zawsze dodaję przy zapisywaniu plików JSON zawierających tekst spoza ASCII. Wersja z escape jest technicznie poprawnym JSON-em, ale o wiele trudniejsza do odczytania w edytorze tekstu.

Serializacja niestandardowych obiektów

Domyślnie json.dumps() nie potrafi serializować instancji niestandardowych klas ani obiektów datetime. Masz dwie opcje: dziedziczenie po json.JSONEncoder lub wcześniejsza konwersja do słownika:

python
import json
from datetime import datetime, date

# Opcja 1: niestandardowa klasa kodera
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"
# }

# Opcja 2: parametr default= (prostszy dla jednorazowych konwersji)
print(json.dumps(data, default=str, indent=2))  # konwertuje nieznane typy na str

Praktyczny wzorzec: wczytywanie pliku konfiguracyjnego

Oto wzorzec z prawdziwego świata, którego używam w niemal każdym projekcie Pythona — moduł ładujący konfigurację z rozsądnymi wartościami domyślnymi:

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)
                # Głębokie scalanie: ustawienia użytkownika nadpisują domyślne
                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 (lub nadpisana wartość)

Podsumowanie

Moduł json Pythona obejmuje wszystko, czego potrzebujesz, bez żadnych zależności. Kluczowe zasady: używaj loads()/dumps() dla łańcuchów, load()/dump() dla plików, zawsze obsługuj JSONDecodeError przy parsowaniu danych zewnętrznych i dodawaj ensure_ascii=False, gdy Twoje dane zawierają znaki spoza alfabetu łacińskiego. Do debugowania danych JSON JSON Formatter i JSON Validator mogą zaoszczędzić Ci dużo czasu.