JSON (JavaScript Object Notation) é um formato leve de intercâmbio de dados, amplamente utilizado em APIs, configurações e armazenamento de dados. Em Python, o módulo json fornece funcionalidades para converter dados Python em JSON e vice-versa. Nesta aula, vamos mergulhar nas principais funções, explorar a serialização de objetos customizados e aprender a lidar com erros comuns.

Compreender JSON é essencial para qualquer desenvolvedor Python, pois é o formato padrão para comunicação entre serviços web, armazenamento de preferências e até mesmo para persistência de dados estruturados. Vamos começar com as funções mais básicas e avançar para técnicas mais avançadas.

json.load e json.dumps

A função json.dumps() (dump string) converte um objeto Python (como dicionário, lista, tupla, string, número, booleano ou None) em uma string JSON. Já json.load() (load string) faz o inverso: converte uma string JSON em um objeto Python. Essas funções são as mais utilizadas quando se trabalha com dados JSON em memória.

Vamos ver um exemplo simples:

import json

# Python dict
dados = {
    "nome": "Alice",
    "idade": 30,
    "cidade": "São Paulo"
}

# Serializar para string JSON
json_string = json.dumps(dados)
print(json_string)  # {"nome": "Alice", "idade": 30, "cidade": "São Paulo"}

# Desserializar de volta para dict
dados_recuperados = json.loads(json_string)
print(dados_recuperados)  # {'nome': 'Alice', 'idade': 30, 'cidade': 'São Paulo'}
print(dados_recuperados["nome"])  # Alice

Note que json.loads() é usado para strings, enquanto json.load() é usado para arquivos. Da mesma forma, json.dumps() para strings e json.dump() para arquivos. Veremos isso em detalhes na seção sobre serialização.

A função json.dumps() aceita parâmetros opcionais como indent para formatação legível, sort_keys para ordenar as chaves, e ensure_ascii para controlar a codificação de caracteres não-ASCII. Exemplo:

import json

dados = {"nome": "João", "idade": 25, "cidade": "Rio de Janeiro"}

# Formatação com indentação e chaves ordenadas
json_pretty = json.dumps(dados, indent=4, sort_keys=True)
print(json_pretty)
# Saída:
# {
#     "cidade": "Rio de Janeiro",
#     "idade": 25,
#     "nome": "João"
# }

Esses parâmetros são úteis para gerar JSON legível para humanos, por exemplo, em arquivos de configuração ou logs.

Serialização

Serialização é o processo de converter um objeto em memória em um formato que possa ser armazenado (em arquivo) ou transmitido (via rede). Em Python, a serialização para JSON é feita com json.dump() (para arquivos) e json.dumps() (para strings). A desserialização é feita com json.load() e json.loads().

Vamos ver como trabalhar com arquivos:

import json

# Dados a serializar
dados = {
    "produtos": [
        {"nome": "Camiseta", "preco": 49.90},
        {"nome": "Calça", "preco": 89.90}
    ]
}

# Serializar para arquivo
with open("dados.json", "w", encoding="utf-8") as arquivo:
    json.dump(dados, arquivo, ensure_ascii=False, indent=4)

# Desserializar do arquivo
with open("dados.json", "r", encoding="utf-8") as arquivo:
    dados_lidos = json.load(arquivo)

print(dados_lidos)
# {'produtos': [{'nome': 'Camiseta', 'preco': 49.9}, {'nome': 'Calça', 'preco': 89.9}]}

É importante usar ensure_ascii=False para preservar caracteres acentuados, e definir a codificação UTF-8 ao abrir o arquivo.

Quando serializamos objetos Python, devemos lembrar que JSON suporta apenas tipos básicos: dicionários, listas, strings, números, booleanos e null. Objetos como datetime, Decimal ou classes personalizadas não são serializáveis por padrão. Para lidar com isso, precisamos criar serializadores customizados, como veremos na próxima seção.

Objetos customizados

Para serializar objetos de classes personalizadas, precisamos fornecer uma função de serialização que converta o objeto em um dicionário ou outro tipo JSON. Isso é feito passando o parâmetro default para json.dumps() ou json.dump(). Essa função é chamada para objetos que não são serializáveis por padrão.

Vamos criar uma classe Pessoa e aprender a serializá-la:

import json

class Pessoa:
    def __init__(self, nome, idade):
        self.nome = nome
        self.idade = idade

def serializar_pessoa(obj):
    if isinstance(obj, Pessoa):
        return {"nome": obj.nome, "idade": obj.idade}
    # Para outros objetos, podemos levantar uma exceção ou retornar None
    raise TypeError(f"Tipo {type(obj)} não serializável")

p = Pessoa("Maria", 28)
json_string = json.dumps(p, default=serializar_pessoa)
print(json_string)  # {"nome": "Maria", "idade": 28}

Para desserializar, precisamos de uma função object_hook que recebe um dicionário e retorna um objeto da classe desejada. Exemplo:

def desserializar_pessoa(dicionario):
    return Pessoa(dicionario["nome"], dicionario["idade"])

p2 = json.loads(json_string, object_hook=desserializar_pessoa)
print(p2.nome)  # Maria
print(p2.idade)  # 28

Outra abordagem é usar a classe json.JSONEncoder e json.JSONDecoder para criar codificadores e decodificadores reutilizáveis. Isso é útil quando precisamos serializar muitos tipos de objetos. Vamos ver um exemplo:

import json

class PessoaEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, Pessoa):
            return {"__pessoa__": True, "nome": obj.nome, "idade": obj.idade}
        return super().default(obj)

class PessoaDecoder(json.JSONDecoder):
    def __init__(self, *args, **kwargs):
        super().__init__(object_hook=self.object_hook, *args, **kwargs)
    def object_hook(self, dicionario):
        if "__pessoa__" in dicionario:
            return Pessoa(dicionario["nome"], dicionario["idade"])
        return dicionario

p = Pessoa("Carlos", 35)
json_str = json.dumps(p, cls=PessoaEncoder)
print(json_str)  # {"__pessoa__": true, "nome": "Carlos", "idade": 35}

p2 = json.loads(json_str, cls=PessoaDecoder)
print(p2.nome)  # Carlos

Essa abordagem é mais robusta e permite marcar objetos com um identificador especial para facilitar a desserialização.

Tratamento de erros

Ao trabalhar com JSON, é comum encontrar erros de parsing ou serialização. O módulo json levanta exceções como json.JSONDecodeError (subclasse de ValueError) quando a string JSON é inválida, e TypeError quando tentamos serializar um objeto não suportado. Vamos ver como lidar com esses erros.

Para capturar erros de parsing, usamos try/except:

import json

json_invalido = '{"nome": "João"'

try:
    dados = json.loads(json_invalido)
except json.JSONDecodeError as e:
    print(f"Erro ao decodificar JSON: {e}")
    print(f"Na linha {e.lineno}, coluna {e.colno}")
    print(f"Mensagem: {e.msg}")
else:
    print("JSON válido")
    print(dados)

Já para erros de serialização, podemos capturar TypeError:

import json

class MinhaClasse:
    pass

obj = MinhaClasse()

try:
    json.dumps(obj)
except TypeError as e:
    print(f"Erro de serialização: {e}")
    # Saída: Erro de serialização: Object of type MinhaClasse is not JSON serializable

Além disso, podemos usar o parâmetro default para evitar que a exceção seja levantada, convertendo objetos não serializáveis em algo serializável, como uma string representativa:

def default_serializer(obj):
    return str(obj)  # Converte para string

json_string = json.dumps(obj, default=default_serializer)
print(json_string)  # "<__main__.MinhaClasse object at 0x...>"

No entanto, é importante ter cuidado com essa abordagem, pois pode mascarar erros.

Boas práticas e observações finais

Ao trabalhar com JSON em Python, siga estas boas práticas:

  • Sempre defina a codificação UTF-8 ao ler ou escrever arquivos JSON para suportar caracteres especiais.
  • Use ensure_ascii=False ao serializar para preservar caracteres não-ASCII.
  • Para objetos customizados, implemente métodos to_dict() e from_dict() na própria classe, facilitando a serialização e desserialização.
  • Valide o JSON recebido de fontes externas antes de usá-lo, para evitar surpresas.
  • Considere usar a biblioteca pydantic ou dataclasses com json para validação e serialização mais robusta em projetos maiores.

JSON é um formato simples, mas poderoso. Com o módulo json do Python, você pode integrar facilmente seus programas com APIs web, arquivos de configuração e muito mais.

Referências

Exercícios

  1. Escreva um script que leia um arquivo JSON chamado config.json (contendo um dicionário com chaves como "host" e "porta") e imprima os valores. Use json.load().
  2. ✓ Resposta:
    import json
    
    with open("config.json", "r", encoding="utf-8") as f:
        config = json.load(f)
    
    print(config["host"])
    print(config["porta"])
  3. Dado o dicionário dados = {"nome": "Ana", "idade": 32, "cidade": "Belo Horizonte"}, serialize-o para uma string JSON com indentação de 2 espaços e chaves em ordem alfabética. Imprima o resultado.
  4. ✓ Resposta:
    import json
    
    dados = {"nome": "Ana", "idade": 32, "cidade": "Belo Horizonte"}
    
    json_string = json.dumps(dados, indent=2, sort_keys=True)
    print(json_string)
  5. Crie uma classe Produto com atributos nome e preco. Implemente um encoder e um decoder para essa classe, e serialize um objeto Produto para JSON e depois desserialize de volta, exibindo os atributos.
  6. ✓ Resposta:
    import json
    
    class Produto:
        def __init__(self, nome, preco):
            self.nome = nome
            self.preco = preco
    
    class ProdutoEncoder(json.JSONEncoder):
        def default(self, obj):
            if isinstance(obj, Produto):
                return {"__produto__": True, "nome": obj.nome, "preco": obj.preco}
            return super().default(obj)
    
    class ProdutoDecoder(json.JSONDecoder):
        def __init__(self, *args, **kwargs):
            super().__init__(object_hook=self.object_hook, *args, **kwargs)
        def object_hook(self, dicionario):
            if "__produto__" in dicionario:
                return Produto(dicionario["nome"], dicionario["preco"])
            return dicionario
    
    p = Produto("Teclado", 150.00)
    json_str = json.dumps(p, cls=ProdutoEncoder)
    print(json_str)
    
    p2 = json.loads(json_str, cls=ProdutoDecoder)
    print(p2.nome, p2.preco)
  7. Escreva um código que tente carregar uma string JSON inválida (por exemplo, "{'nome': 'João'}") e capture a exceção, imprimindo uma mensagem amigável.
  8. ✓ Resposta:
    import json
    
    json_invalido = "{'nome': 'João'}"
    
    try:
        dados = json.loads(json_invalido)
    except json.JSONDecodeError as e:
        print("JSON inválido!")
        print(f"Erro: {e.msg} na linha {e.lineno}, coluna {e.colno}")
    else:
        print(dados)
  9. Dada uma lista de dicionários representando produtos, serialize a lista para um arquivo chamado produtos.json com indentação e sem caracteres ASCII escapados. Depois, leia o arquivo e exiba o número de produtos.
  10. ✓ Resposta:
    import json
    
    produtos = [
        {"nome": "Camiseta", "preco": 49.90},
        {"nome": "Calça", "preco": 89.90}
    ]
    
    with open("produtos.json", "w", encoding="utf-8") as f:
        json.dump(produtos, f, ensure_ascii=False, indent=4)
    
    with open("produtos.json", "r", encoding="utf-8") as f:
        dados = json.load(f)
    
    print(len(dados))  # 2