Python e JSON formam uma dupla natural. Seja construindo uma API REST com FastAPI ou Django, processando pipelines de dados, ou apenas lendo um arquivo de configuração, você vai trabalhar com JSON constantemente. A boa notícia: a biblioteca padrão do Python tem tudo que você precisa no módulo json. Sem necessidade de pip install.

As Quatro Funções que Você Realmente Usa

O módulo json oferece quatro funções para o dia a dia:

  • json.loads(str) — analisa uma string JSON em um objeto Python
  • json.dumps(obj) — converte um objeto Python em uma string JSON
  • json.load(file) — analisa JSON diretamente de um objeto arquivo
  • json.dump(obj, file) — escreve um objeto Python como JSON em um arquivo

O s em loads / dumps significa string. As que não têm o s trabalham com objetos de arquivo. Fácil de lembrar quando você conhece a regra.

json.loads() — Analisando uma String 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'>

Observe o mapeamento de tipos: JSON true vira Python True, JSON false vira Python False, JSON null vira Python None. Objetos JSON viram Python dict, arrays JSON viram Python list.

json.dumps() — Serializando para uma String JSON

python
import json

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

# Compacto (bom para transmissão em rede)
compact = json.dumps(user)
print(compact)
# {"name": "Bob", "age": 25, "roles": ["admin", "editor"], "active": true, "extra": null}

# Formatado (bom para logs e inspeção humana)
pretty = json.dumps(user, indent=2)
print(pretty)
# {
#   "name": "Bob",
#   "age": 25,
#   "roles": [
#     "admin",
#     "editor"
#   ],
#   "active": true,
#   "extra": null
# }

Observe o mapeamento inverso de tipos: Python True → JSON true, Python None → JSON null. O Python lida com isso automaticamente.

Lendo JSON de um Arquivo

Este é provavelmente o caso de uso mais comum — ler um arquivo de configuração ou dados na inicialização:

python
import json

# Lê e analisa em uma etapa
with open("config.json", "r", encoding="utf-8") as f:
    config = json.load(f)

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

Sempre especifique encoding="utf-8" ao abrir arquivos JSON. O JSON é especificado como UTF-8 pela RFC 8259, e omiti-lo pode causar problemas no Windows, onde o encoding padrão às vezes é cp1252.

Escrevendo JSON em um Arquivo

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

Tratando Erros Corretamente

json.loads() lança json.JSONDecodeError (uma subclasse de ValueError) quando a entrada não é um JSON válido. Sempre trate isso ao analisar dados que você não controla:

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"}')   # funciona normalmente
bad  = safe_parse('not json at all')     # imprime erro, retorna None
also_bad = safe_parse('{"key": }')       # imprime erro com informação de posição

JSONDecodeError fornece a linha e coluna exatas onde a análise falhou, o que é útil ao depurar arquivos JSON grandes.

Opções Úteis do dumps()

python
import json

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

# Ordenar chaves alfabeticamente (ótimo para saída reproduzível / diffs)
print(json.dumps(data, sort_keys=True, indent=2))
# {
#   "a_key": 2,
#   "price": 9.999999999,
#   "z_key": 1
# }

# Garantir que caracteres não-ASCII sejam preservados (padrão: escapados para \uXXXX)
data2 = {"city": "Münich", "greeting": "こんにちは"}
print(json.dumps(data2, ensure_ascii=False))
# {"city": "Münich", "greeting": "こんにちは"}

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

ensure_ascii=False é algo que sempre adiciono ao escrever arquivos JSON que contêm texto não-ASCII. A versão escapada é tecnicamente JSON válido, mas muito mais difícil de ler em um editor de texto.

Serializando Objetos Personalizados

Por padrão, json.dumps() não consegue serializar instâncias de classes personalizadas ou objetos datetime. Você tem duas opções: criar uma subclasse de json.JSONEncoder, ou converter para um dict primeiro:

python
import json
from datetime import datetime, date

# Opção 1: classe de encoder personalizada
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"
# }

# Opção 2: parâmetro default= (mais simples para conversões pontuais)
print(json.dumps(data, default=str, indent=2))  # converte qualquer tipo desconhecido para str

Um Padrão Prático: Carregando Arquivo de Configuração

Aqui está um padrão do mundo real que uso em quase todo projeto Python — um carregador de configuração que lê um arquivo de configuração JSON com valores padrão sensatos:

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)
                # Mesclagem profunda: configurações do usuário substituem os padrões
                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 (ou valor substituído)

Conclusão

O módulo json do Python cobre tudo que você precisa sem dependências externas. As regras principais: use loads()/dumps() para strings, load()/dump() para arquivos, sempre trate JSONDecodeError ao analisar dados externos, e adicione ensure_ascii=False quando seus dados contiverem caracteres não-latinos. Para depurar dados JSON, o Formatador JSON e o Validador JSON podem economizar muito tempo.