Trilha de aprendizado · Nível 6 · Tutorial 6

Salvar e carregar dados em JSON

Persistir coleções compatíveis em JSON e recuperar seus dados, distinguindo erros de sintaxe, incompatibilidades de valores e problemas na estrutura esperada.

  • Nível: Intermediário
  • Duração: 25 min
  • 8 passos
Salvar e carregar dados em JSON

O que você vai percorrer

  1. Reconhecer a estrutura e os valores de JSON Interprete a estrutura de um documento JSON, reconheça sua sintaxe e associe seus valores aos tipos básicos de Python. 3 min
  2. Converter entre dados Python e strings JSON Use json.dumps e json.loads para fazer uma ida e volta entre uma coleção Python e sua representação como texto JSON, inteiramente em memória. 3 min
  3. Entender os limites da ida e volta Observe quais características dos valores Python são alteradas ou rejeitadas em uma ida e volta por JSON. 3 min
  4. Produzir JSON legível com acentos Compare formas de apresentar o mesmo documento JSON e diferencie escapes de caracteres da codificação usada ao gravar texto. 2 min
  5. Salvar e carregar um documento por arquivo Use as interfaces de arquivo do módulo json, mantenha um único documento por arquivo e reduza o risco de truncar o destino antes de detectar valores incompatíveis. 4 min
  6. Diagnosticar falhas de leitura e de sintaxe Diferencie problemas de acesso, decodificação e interpretação do JSON e use a posição informada por JSONDecodeError para investigar a sintaxe. 3 min
  7. Validar os dados antes de usá-los Verifique a estrutura, os campos e os valores de uma coleção de tarefas depois da leitura do JSON e antes de processar ou modificar os registros. 4 min
  8. Aplicação final: atualizar tarefas e conferir a persistência Execute localmente o ciclo completo de leitura, validação, atualização, gravação e conferência de uma coleção de tarefas, depois compare falhas de acesso, sintaxe, estrutura e serialização. 5 min

O que você vai aprender

  • Relacionar os tipos de valores JSON aos tipos Python correspondentes.
  • Serializar e desserializar dados com as funções do módulo json para textos e arquivos.
  • Configurar uma saída legível sem confundir representação de caracteres com codificação do arquivo.
  • Validar a estrutura recuperada antes de acessar campos ou processar registros.
  • Tratar separadamente JSON inválido, falhas de acesso e dados incompatíveis com o uso esperado.

Antes de começar

  • Ler e gravar arquivos de texto com with
  • Validar entradas e sinalizar falhas com raise
  • Consultar e atualizar coleções aninhadas

Passo 1 de 8

Reconhecer a estrutura e os valores de JSON

Interprete a estrutura de um documento JSON, reconheça sua sintaxe e associe seus valores aos tipos básicos de Python.

Dados representados como texto

JSON não é um literal Python

JSON é um formato textual de intercâmbio de dados. Ele permite representar valores e coleções de maneira independente do programa que os produziu.

Serializar é transformar valores em uma representação textual. Desserializar é interpretar essa representação para reconstruir valores utilizáveis pelo programa.

Embora um documento JSON possa lembrar listas e dicionários de Python, ele segue uma sintaxe própria.

Estrutura aninhada de uma coleção

A estrutura pode ser lida do contêiner externo para os valores internos.

Diagrama de um objeto raiz que contém um array com dois objetos de tarefa; cada tarefa apresenta visualmente um título, um estado de conclusão e uma prioridade.

Um objeto raiz contém um array; cada posição do array contém outro objeto com os dados de uma tarefa.

Lendo um documento JSON

Sintaxe que identifica JSON

Em JSON, as chaves de um objeto são textos entre aspas duplas. Strings também usam aspas duplas. Os valores booleanos são true e false, e a ausência de valor é null, sempre em minúsculas.

Comentários não fazem parte da sintaxe, e não pode haver vírgula depois do último elemento de um objeto ou array.

Coleção de tarefas em JSON

Leia de fora para dentro: objeto raiz, array tarefas e objetos individuais.

json
{
  "tarefas": [
    {
      "titulo": "Revisar JSON",
      "concluida": false,
      "prioridade": 2
    },
    {
      "titulo": "Enviar relatório",
      "concluida": true,
      "prioridade": 1
    }
  ],
  "proxima_revisao": null
}

Exemplo

Correspondências principais

Ao reconstruir esses dados em Python:

  • object corresponde a dict;
  • array corresponde a list;
  • string corresponde a str;
  • número inteiro corresponde a int;
  • número fracionário corresponde a float;
  • true e false correspondem a True e False, do tipo bool;
  • null corresponde a None.

O nome object pertence ao vocabulário do JSON; nesta correspondência, ele indica uma coleção de pares chave–valor como um dicionário.

Associe os tipos

JSON e Python

Associe cada tipo ou valor JSON ao correspondente em Python.

Toque em um item e depois no par correspondente.

Identifique um documento JSON

Qual trecho segue a sintaxe JSON?

Escolha o único documento JSON sintaticamente correto.

Passo 2 de 8

Converter entre dados Python e strings JSON

Use json.dumps e json.loads para fazer uma ida e volta entre uma coleção Python e sua representação como texto JSON, inteiramente em memória.

Duas operações, duas direções

Coleção ou texto?

O módulo json faz parte da biblioteca padrão: basta usar import json, sem instalar dependências.

  • json.dumps(dados) recebe um valor Python compatível e devolve uma str contendo JSON.
  • json.loads(texto) recebe uma str contendo um documento JSON e devolve o valor Python correspondente.

O dicionário e a string JSON podem representar os mesmos dados, mas são valores de tipos diferentes. Nesta etapa, toda a conversão acontece em memória, sem leitura ou gravação de arquivos.

Fluxo de ida e volta

Diagrama em que um dicionário Python passa por json.dumps e se torna uma string JSON; a string passa por json.loads e volta a ser um dicionário.

dumps: valor Python → string JSON. loads: string JSON → valor Python.

Ida e volta em memória

Execute no seu computador

Este exemplo transforma a coleção em texto JSON e depois reconstrói uma coleção Python a partir desse texto.

python
import json

colecao = {
    "tarefas": [
        {
            "titulo": "Revisar JSON",
            "concluida": False,
            "prioridade": 4,
        },
        {
            "titulo": "Praticar conversoes",
            "concluida": True,
            "prioridade": 2,
        },
    ]
}

texto_json = json.dumps(colecao)
colecao_recuperada = json.loads(texto_json)

print(type(colecao).__name__)
print(type(texto_json).__name__)
print(type(colecao_recuperada).__name__)
print(colecao_recuperada["tarefas"][0]["titulo"])

Exemplo

Resultado esperado

As linhas finais exibem, nesta ordem:

dict

str

dict

Revisar JSON

texto_json não é outro dicionário: é uma string que contém um documento JSON. Depois de loads, colecao_recuperada volta a oferecer acesso por chaves e índices.

Escolha a conversão correta

Complete a ida e volta

Considere que dados é um dicionário compatível com JSON. Qual alternativa completa corretamente a conversão e prevê os tipos de texto e copia?

Preveja o tipo intermediário

Complete com o nome do tipo: após texto = json.dumps(dados), type(texto).__name__ resulta em _.

Passo 3 de 8

Entender os limites da ida e volta

Observe quais características dos valores Python são alteradas ou rejeitadas em uma ida e volta por JSON.

Compatível não significa idêntico

O formato define o que pode voltar

Uma ida e volta executa json.dumps(dados) e depois json.loads(texto). Mesmo quando as duas operações funcionam, o valor recuperado pode não ter exatamente os mesmos tipos do original.

Isso acontece porque JSON tem menos tipos que Python: objetos JSON só possuem chaves textuais, e arrays não distinguem listas de tuplas.

Tipos antes e depois

Diagrama mostrando uma tupla transformada em lista e uma chave inteira transformada em chave textual durante uma ida e volta por JSON.

A representação intermediária em JSON não registra que um array veio de uma tupla nem que uma chave textual veio de um inteiro.

Chaves inteiras e tuplas mudam

Confira a ida e volta

Execute o código e compare os tipos antes e depois da conversão.

python
import json

original = {
    1: ("estudar", "praticar")
}

texto = json.dumps(original)
recuperado = json.loads(texto)

print(texto)
print(recuperado)
print(type(next(iter(recuperado))))
print(type(recuperado["1"]))

# Saída:
# {"1": ["estudar", "praticar"]}
# {'1': ['estudar', 'praticar']}
# <class 'str'>
# <class 'list'>

Dica

Preveja antes de acessar

Depois de loads, a chave disponível é "1", e não 1. O array volta como list, pois o documento JSON não contém informação suficiente para reconstruir a tupla.

Conjuntos exigem uma decisão explícita

Não há conjunto em JSON

O módulo json não possui uma representação direta para set. Tentar serializar um conjunto provoca TypeError.

Você pode convertê-lo explicitamente para uma lista, mas essa escolha altera o significado disponível após a recuperação: a lista não expressa unicidade e sua ordem pode não ser significativa quando a origem era um conjunto.

Falha e conversão consciente

python
import json

etiquetas = {"python", "json"}

try:
    json.dumps(etiquetas)
except TypeError as erro:
    print(f"Não foi possível serializar: {erro}")

# Conversão explícita: adequada somente se uma lista servir ao uso futuro.
texto = json.dumps(list(etiquetas))
recuperado = json.loads(texto)

print(recuperado)
print(type(recuperado))  # <class 'list'>

Atenção

A conversão tem consequências

Converter um conjunto para lista não preserva automaticamente seu tipo nem a regra de unicidade. Se a ordem da saída for importante, ela também precisa ser definida de forma consciente antes da serialização.

Verifique suas previsões

Chaves após a ida e volta

Após json.loads(json.dumps({1: "um"})), o valor recuperado pode ser acessado com a chave inteira 1.

Tuplas após a ida e volta

Uma tupla serializada como array JSON volta a ser uma tupla depois de loads.

Serialização de conjuntos

Passar um set diretamente para json.dumps provoca TypeError.

Passo 4 de 8

Produzir JSON legível com acentos

Compare formas de apresentar o mesmo documento JSON e diferencie escapes de caracteres da codificação usada ao gravar texto.

Organização visual com indent

Os dados não mudam

O parâmetro indent organiza objetos e arrays em várias linhas, com recuo para indicar os níveis da estrutura. Ele altera apenas a apresentação do documento: os valores representados continuam os mesmos.

Três apresentações do mesmo dado

Execute o código e compare as saídas. Sem indent, o JSON fica em uma linha. Com indent=2, cada nível recebe dois espaços de recuo.

python
import json

dados = {
    "titulo": "Revisão de português",
    "concluida": False,
}

print("SEM INDENTAÇÃO")
print(json.dumps(dados))

print("\nINDENTADO COM ESCAPES")
print(json.dumps(dados, indent=2))

print("\nINDENTADO COM ACENTOS VISÍVEIS")
print(json.dumps(dados, indent=2, ensure_ascii=False))

Apresentação e representação dos caracteres

Diagrama comparando a mesma estrutura JSON em uma linha e com níveis indentados, além de caracteres acentuados representados por escapes e diretamente.

A indentação evidencia a hierarquia. A escolha entre escapes e caracteres visíveis muda a representação textual, não os dados.

Escapes e acentos visíveis

O papel de ensure_ascii

Por padrão, ensure_ascii=True representa caracteres fora do conjunto ASCII usando escapes como \u00e3. Com ensure_ascii=False, caracteres como ã e ê aparecem diretamente na string JSON. As duas formas são JSON válido.

A desserialização recupera o mesmo texto

Os dois documentos têm representações diferentes, mas json.loads reconstrói valores Python iguais.

python
import json

texto_com_escapes = r'{"titulo": "Revis\u00e3o de portugu\u00eas"}'
texto_com_acentos = '{"titulo": "Revisão de português"}'

primeiro = json.loads(texto_com_escapes)
segundo = json.loads(texto_com_acentos)

print(primeiro)
print(segundo)
print(primeiro == segundo)  # True

Exemplo

Duas escritas, um mesmo valor

Depois de loads, tanto "Revis\u00e3o" quanto "Revisão" produzem a string Python "Revisão". O escape pertence à representação JSON; ele não permanece como parte do conteúdo recuperado.

Representação não é codificação

Duas decisões independentes

json.dumps sempre devolve uma str, inclusive quando usamos ensure_ascii=False. Esse parâmetro decide como caracteres não ASCII aparecem dentro do texto JSON; ele não transforma a string em bytes nem escolhe a codificação de um arquivo.

Ao gravar esse texto posteriormente, a codificação — por exemplo, UTF-8 — ainda precisa ser definida na operação de arquivo. Portanto, acentos visíveis e codificação UTF-8 são decisões relacionadas, mas distintas.

Dica

Pergunte em qual camada está a opção

indent organiza a aparência, ensure_ascii controla a representação de caracteres no JSON e encoding="utf-8" controla a conversão entre texto e bytes durante o acesso ao arquivo.

Escolha a configuração adequada

JSON legível em memória

Qual chamada produz uma str JSON com recuo de dois espaços e acentos visíveis?

Passo 5 de 8

Salvar e carregar um documento por arquivo

Use as interfaces de arquivo do módulo json, mantenha um único documento por arquivo e reduza o risco de truncar o destino antes de detectar valores incompatíveis.

Strings ou arquivos?

As quatro funções

O módulo json oferece duas interfaces:

  • json.dumps(dados) produz uma str com JSON.
  • json.loads(texto) interpreta uma str com JSON.
  • json.dump(dados, arquivo) escreve JSON em um arquivo de texto aberto.
  • json.load(arquivo) lê JSON de um arquivo de texto aberto.

A letra s em dumps e loads ajuda a lembrar da interface com strings. Sem o s, a função recebe também o objeto de arquivo aberto.

Do valor Python ao destino correto

Diagrama comparando a conversão entre dados Python e uma string JSON com dumps e loads, e entre dados Python e um arquivo aberto com dump e load.

dumps e loads trabalham com texto em memória; dump e load, com um arquivo aberto.

Gravar e recuperar com UTF-8

Um documento completo por arquivo

Um arquivo JSON convencional guarda um único documento completo. Esse documento pode conter muitos registros: neste exemplo, o objeto raiz possui uma lista em tarefas.

O arquivo continua sendo texto. Por isso, abra-o com encoding="utf-8". indent organiza a apresentação e ensure_ascii=False permite exibir os caracteres acentuados diretamente; nenhum deles substitui a escolha da codificação.

Salvar com dump e carregar com load

Execute o código no seu computador. Ele cria tarefas.json na pasta de trabalho atual e depois recupera seu conteúdo.

python
from pathlib import Path
import json

caminho = Path("tarefas.json")
dados = {
    "tarefas": [
        {"titulo": "Revisar função", "concluida": False, "prioridade": 3},
        {"titulo": "Enviar relatório", "concluida": True, "prioridade": 5},
    ]
}

with caminho.open("w", encoding="utf-8") as arquivo:
    json.dump(dados, arquivo, indent=2, ensure_ascii=False)

with caminho.open("r", encoding="utf-8") as arquivo:
    recuperados = json.load(arquivo)

print(recuperados)
print(recuperados == dados)

Dica

Observe as responsabilidades

Path.open abre o arquivo e with garante seu fechamento. json.dump e json.load cuidam da representação JSON, mas recebem o objeto de arquivo já aberto no modo adequado.

Atualizar não é concatenar

Regrave a coleção completa

Para atualizar uma tarefa, carregue o documento, altere a coleção em memória e grave novamente todo o documento em modo w.

Abrir em modo a e chamar dump outra vez não insere um item no array existente. Isso apenas coloca um segundo documento após o primeiro, produzindo algo semelhante a {...}{...}. O resultado deixa de ser um único documento adequado para json.load.

Atenção

Não use dump sucessivo como acréscimo de registro

json.dump(primeiro, arquivo) seguido de json.dump(segundo, arquivo) não cria automaticamente uma lista com dois elementos. Para reunir registros, coloque-os em uma lista Python e serialize a coleção completa uma única vez.

Como acrescentar uma tarefa corretamente?

O arquivo contém {"tarefas": [...]}. Qual procedimento mantém um único documento JSON válido?

Serializar antes de truncar

Detecte incompatibilidades antes do modo w

json.dump pode escrever parte do documento antes de encontrar um valor não serializável. Como o modo w também trunca o arquivo ao abri-lo, uma falha pode deixar o destino incompleto.

Uma precaução é gerar primeiro toda a string com json.dumps. Se ocorrer TypeError, isso acontece antes da abertura do destino. Depois, abra o arquivo e escreva a string pronta.

Essa estratégia reduz um risco específico, mas não protege contra falhas durante a própria escrita, como falta de espaço ou perda de acesso ao arquivo.

Preparar o texto antes da gravação

A serialização ocorre antes de o arquivo ser aberto em modo w.

python
from pathlib import Path
import json

caminho = Path("tarefas.json")

# Se houver um valor incompatível, TypeError ocorre aqui.
texto_json = json.dumps(
    dados,
    indent=2,
    ensure_ascii=False,
)

# O destino só é truncado depois da serialização bem-sucedida.
with caminho.open("w", encoding="utf-8") as arquivo:
    arquivo.write(texto_json)

Ordene um fluxo de atualização prudente

Coloque as ações na ordem correta para atualizar a coleção e verificar sua serialização antes de truncar o arquivo.

  1. Alterar a coleção recuperada em memória.
  2. Gerar a representação completa com `json.dumps`.
  3. Abrir o arquivo em modo `r` e carregar o documento com `json.load`.
  4. Abrir o destino em modo `w`, com UTF-8.
  5. Escrever a string JSON preparada no arquivo.

Passo 6 de 8

Diagnosticar falhas de leitura e de sintaxe

Diferencie problemas de acesso, decodificação e interpretação do JSON e use a posição informada por JSONDecodeError para investigar a sintaxe.

Em qual etapa ocorreu a falha?

Três etapas, três diagnósticos

Ao carregar JSON, três operações podem falhar por motivos diferentes:

  1. Acessar o arquivo: caminho inexistente, permissão negada ou outro problema do sistema gera OSError ou uma de suas subclasses.
  2. Decodificar os bytes como texto: conteúdo incompatível com UTF-8 gera UnicodeDecodeError.
  3. Interpretar o texto como JSON: sintaxe inválida gera json.JSONDecodeError.

Capturas específicas e próximas da operação responsável preservam essa distinção. Uma única mensagem como “não foi possível carregar” esconderia a causa real.

Fluxo de diagnóstico

Diagrama em três etapas: acesso ao arquivo, decodificação UTF-8 e interpretação JSON, com uma possível falha em cada etapa.

A falha deve ser associada à etapa que realmente a produziu: acesso, decodificação ou interpretação.

Inspecione JSONDecodeError

Mensagem e localização

JSONDecodeError significa que o texto foi lido, mas não pôde ser interpretado como um documento JSON completo. O objeto da exceção oferece informações úteis:

  • msg: descrição do que o decodificador esperava;
  • lineno: linha em que ele detectou o problema;
  • colno: coluna dessa linha.

A posição mostra onde a interpretação parou. Comece por ela, mas examine também o caractere anterior: uma vírgula ou aspas incorretas podem ter causado a falha percebida logo depois.

Separando as etapas no código

Este exemplo lê o texto antes de interpretá-lo para deixar explícita a origem de cada falha.

python
import json
from pathlib import Path


def carregar_json(caminho):
    try:
        texto = Path(caminho).read_text(encoding="utf-8")
    except UnicodeDecodeError as erro:
        print(f"O arquivo não contém texto UTF-8 válido: {erro}")
        raise
    except OSError as erro:
        print(f"Não foi possível acessar o arquivo: {erro}")
        raise

    try:
        return json.loads(texto)
    except json.JSONDecodeError as erro:
        print(f"JSON inválido: {erro.msg}")
        print(f"Localização: linha {erro.lineno}, coluna {erro.colno}")
        raise


dados = carregar_json("tarefas.json")
print(dados)

Dica

O que a localização não faz

Linha e coluna orientam a inspeção, mas não corrigem o documento nem explicam regras da aplicação. Elas apenas indicam onde o decodificador deixou de reconhecer uma sintaxe JSON válida.

Sintomas frequentes

Exemplo

Quatro formas de JSON inválido

Aspas inadequadas: {'titulo': 'Estudar'} usa aspas simples; JSON exige aspas duplas em chaves e strings.

Vírgula final: {"tarefas": [1, 2,]} contém uma vírgula sem um próximo elemento.

Arquivo vazio: não existe valor JSON para interpretar; a falha costuma ser indicada logo na linha 1, coluna 1.

Documentos concatenados: {"a": 1}{"b": 2} contém dois documentos completos em sequência. Depois do primeiro, o decodificador encontra dados extras.

Atenção

Não transforme falha em dado válido

Evite capturar uma falha de leitura ou sintaxe e retornar silenciosamente [] ou {}. Isso confunde “não foi possível carregar” com “o arquivo contém uma coleção vazia” e pode levar o programa a sobrescrever dados existentes. Informe a causa ou permita que a exceção continue até o ponto responsável pelo tratamento.

Pratique o diagnóstico

Relacione falhas e diagnósticos

Associe cada situação ao diagnóstico mais específico.

Toque em um item e depois no par correspondente.

Use a localização do erro

Uma exceção informa Expecting value, linha 4, coluna 3. Qual é a melhor ação inicial?

Passo 7 de 8

Validar os dados antes de usá-los

Verifique a estrutura, os campos e os valores de uma coleção de tarefas depois da leitura do JSON e antes de processar ou modificar os registros.

Sintaxe válida não garante dados adequados

Do documento ao contrato da aplicação

json.load ou json.loads confirma apenas que o documento segue a sintaxe JSON. O resultado ainda pode ser um número, uma lista inesperada ou um objeto com registros incompletos.

Nesta aplicação, o contrato esperado é:

  1. a raiz é um dict;
  2. a raiz contém o campo tarefas;
  3. tarefas é uma list;
  4. cada elemento da lista é um dict;
  5. cada tarefa possui titulo, concluida e prioridade com tipos e valores válidos.

A validação deve seguir essa ordem, do contêiner externo para os dados internos. Assim, o programa não tenta acessar campos de uma estrutura que ainda não foi confirmada.

Camadas da validação

Diagrama em camadas mostrando um objeto raiz, uma lista de tarefas, registros individuais e seus três campos, com verificações sucessivas antes do uso.

Valide primeiro a raiz, depois a lista, cada registro e, por último, os campos e seus valores.

Implementar o contrato em uma função

Tipos, presença e valores

Use TypeError quando o valor tem um tipo inadequado. Use ValueError quando falta um campo obrigatório ou quando um valor do tipo correto viola uma regra.

A prioridade merece uma verificação explícita: como bool também é aceito por isinstance(valor, int), teste e rejeite booleanos antes de aceitar inteiros.

Validador da coleção de tarefas

A função termina sem retorno quando toda a coleção é válida. A primeira violação encontrada gera uma mensagem com o índice do registro.

python
def validar_colecao(dados):
    if not isinstance(dados, dict):
        raise TypeError("a raiz deve ser um dict")

    if "tarefas" not in dados:
        raise ValueError("a raiz deve conter o campo 'tarefas'")

    tarefas = dados["tarefas"]
    if not isinstance(tarefas, list):
        raise TypeError("o campo 'tarefas' deve ser uma list")

    campos_obrigatorios = ("titulo", "concluida", "prioridade")

    for indice, tarefa in enumerate(tarefas):
        if not isinstance(tarefa, dict):
            raise TypeError(f"tarefa {indice}: o registro deve ser um dict")

        for campo in campos_obrigatorios:
            if campo not in tarefa:
                raise ValueError(
                    f"tarefa {indice}: campo obrigatório ausente: {campo}"
                )

        titulo = tarefa["titulo"]
        if not isinstance(titulo, str):
            raise TypeError(f"tarefa {indice}: 'titulo' deve ser str")
        if not titulo.strip():
            raise ValueError(f"tarefa {indice}: 'titulo' não pode ser vazio")

        concluida = tarefa["concluida"]
        if not isinstance(concluida, bool):
            raise TypeError(f"tarefa {indice}: 'concluida' deve ser bool")

        prioridade = tarefa["prioridade"]
        if isinstance(prioridade, bool) or not isinstance(prioridade, int):
            raise TypeError(
                f"tarefa {indice}: 'prioridade' deve ser int, não bool"
            )
        if not 1 <= prioridade <= 5:
            raise ValueError(
                f"tarefa {indice}: 'prioridade' deve estar entre 1 e 5"
            )

Dica

Valide antes de alterar

Conclua a validação de toda a coleção antes de processar ou modificar qualquer tarefa. Isso evita que uma falha encontrada no final deixe alterações parciais nos registros anteriores.

Separar interpretação e validação

Falhas de etapas diferentes

Um JSONDecodeError pertence à interpretação do documento. Já TypeError e ValueError produzidos por validar_colecao pertencem ao contrato da aplicação.

Essa separação é especialmente importante porque JSONDecodeError deriva de ValueError. Um único except ValueError envolvendo leitura e validação esconderia a origem da falha.

Tratar cada etapa no ponto responsável

Execute este trecho no mesmo arquivo da função validar_colecao. A validação só ocorre quando leitura e interpretação terminam com sucesso.

python
import json
from pathlib import Path

caminho = Path("tarefas.json")

try:
    with caminho.open("r", encoding="utf-8") as arquivo:
        dados = json.load(arquivo)
except OSError as erro:
    print(f"Falha ao acessar o arquivo: {erro}")
except UnicodeDecodeError as erro:
    print(f"Falha ao decodificar o texto: {erro}")
except json.JSONDecodeError as erro:
    print(
        f"JSON inválido na linha {erro.lineno}, "
        f"coluna {erro.colno}: {erro.msg}"
    )
else:
    try:
        validar_colecao(dados)
    except (TypeError, ValueError) as erro:
        print(f"Dados incompatíveis com a aplicação: {erro}")
    else:
        print("Coleção válida e pronta para uso.")

Exemplo

Mesmo JSON, resultados diferentes

42 é um documento JSON sintaticamente válido, mas o validador o rejeita porque a raiz não é um dict.

{"tarefas": [{"titulo": "Revisar", "concluida": false}]} também é JSON válido, mas viola o contrato porque falta o campo prioridade.

Em nenhum dos casos a solução é substituir silenciosamente os dados por uma lista vazia: a falha deve ser informada.

Diagnostique antes de usar

Sintaxe ou estrutura?

O conteúdo do arquivo é apenas 42. O que acontece ao aplicar o fluxo apresentado?

Localize três violações

Para cada caso, indique qual verificação do validador deve rejeitá-lo e se a exceção é TypeError ou ValueError:

  1. uma tarefa sem o campo prioridade;
  2. uma tarefa com "prioridade": true;
  3. uma tarefa com "titulo": " ".

Escreva pelo menos 80 caracteres (0/80).

Passo 8 de 8

Aplicação final: atualizar tarefas e conferir a persistência

Execute localmente o ciclo completo de leitura, validação, atualização, gravação e conferência de uma coleção de tarefas, depois compare falhas de acesso, sintaxe, estrutura e serialização.

Prepare o documento e visualize o ciclo

Arquivo inicial

Crie uma pasta de treino e, dentro dela, salve o conteúdo abaixo como tarefas.json. Depois, crie o script atualizar_tarefas.py na mesma pasta. A prática seguirá este ciclo: carregar, validar, alterar, validar novamente, serializar, gravar um único documento e reabrir para conferir.

Ciclo completo da persistência

Observe que a gravação só ocorre depois que os dados carregados e os dados alterados atravessam suas validações.

Diagrama do arquivo JSON passando por leitura, validação, alteração, nova validação, serialização, gravação e reabertura.

O arquivo só é substituído depois das verificações e da serialização bem-sucedidas.

tarefas.json

Salve exatamente este documento inicial em UTF-8:

json
{
  "tarefas": [
    {
      "titulo": "Revisar persistência",
      "concluida": false,
      "prioridade": 4
    },
    {
      "titulo": "Conferir relatório",
      "concluida": false,
      "prioridade": 2
    }
  ]
}

Execute o ciclo completo

Script de atualização

Salve o código como atualizar_tarefas.py e execute-o no terminal. Ele altera a primeira tarefa para concluída. A string JSON é produzida antes da abertura em modo w, evitando truncar o arquivo quando a serialização falha.

atualizar_tarefas.py

O script separa parsing, validação, serialização, escrita e conferência final:

python
import json
from pathlib import Path

CAMINHO = Path("tarefas.json")


def validar_colecao(dados):
    if not isinstance(dados, dict):
        raise TypeError("a raiz deve ser um dict")

    if "tarefas" not in dados:
        raise ValueError("o campo 'tarefas' é obrigatório")

    tarefas = dados["tarefas"]
    if not isinstance(tarefas, list):
        raise TypeError("o campo 'tarefas' deve ser uma list")

    obrigatorios = ("titulo", "concluida", "prioridade")

    for indice, tarefa in enumerate(tarefas):
        referencia = f"tarefas[{indice}]"

        if not isinstance(tarefa, dict):
            raise TypeError(f"{referencia} deve ser um dict")

        for campo in obrigatorios:
            if campo not in tarefa:
                raise ValueError(
                    f"{referencia}: o campo {campo!r} é obrigatório"
                )

        titulo = tarefa["titulo"]
        if not isinstance(titulo, str):
            raise TypeError(f"{referencia}.titulo deve ser str")
        if not titulo.strip():
            raise ValueError(f"{referencia}.titulo não pode ser vazio")

        concluida = tarefa["concluida"]
        if not isinstance(concluida, bool):
            raise TypeError(f"{referencia}.concluida deve ser bool")

        prioridade = tarefa["prioridade"]
        if isinstance(prioridade, bool) or not isinstance(prioridade, int):
            raise TypeError(f"{referencia}.prioridade deve ser int")
        if not 1 <= prioridade <= 5:
            raise ValueError(
                f"{referencia}.prioridade deve estar entre 1 e 5"
            )


def carregar(caminho):
    with caminho.open("r", encoding="utf-8") as arquivo:
        return json.load(arquivo)


def executar(caminho):
    try:
        dados = carregar(caminho)
    except OSError as erro:
        print(f"Falha de acesso: {erro}")
        return
    except UnicodeDecodeError as erro:
        print(f"Falha ao decodificar o arquivo como UTF-8: {erro}")
        return
    except json.JSONDecodeError as erro:
        print(
            "JSON inválido: "
            f"{erro.msg}, linha {erro.lineno}, coluna {erro.colno}"
        )
        return

    try:
        validar_colecao(dados)
    except (TypeError, ValueError) as erro:
        print(f"Dados incompatíveis com a aplicação: {erro}")
        return

    dados["tarefas"][0]["concluida"] = True
    validar_colecao(dados)

    try:
        texto_json = json.dumps(
            dados,
            indent=2,
            ensure_ascii=False,
        )
    except TypeError as erro:
        print(f"Falha de serialização: {erro}")
        return

    try:
        with caminho.open("w", encoding="utf-8") as arquivo:
            arquivo.write(texto_json)
            arquivo.write("\n")
    except OSError as erro:
        print(f"Falha de gravação: {erro}")
        return

    try:
        confirmacao = carregar(caminho)
        validar_colecao(confirmacao)
    except OSError as erro:
        print(f"Falha ao reabrir o arquivo: {erro}")
        return
    except UnicodeDecodeError as erro:
        print(f"Falha ao decodificar a conferência: {erro}")
        return
    except json.JSONDecodeError as erro:
        print(
            "JSON inválido na conferência: "
            f"{erro.msg}, linha {erro.lineno}, coluna {erro.colno}"
        )
        return
    except (TypeError, ValueError) as erro:
        print(f"Dados inválidos na conferência: {erro}")
        return

    primeira = confirmacao["tarefas"][0]
    print(f"Título reaberto: {primeira['titulo']}")
    print(f"Concluída após reabrir: {primeira['concluida']}")


if __name__ == "__main__":
    executar(CAMINHO)

Atenção

Não teste falhas sobre sua única cópia

Use arquivos separados nos próximos testes. Uma entrada malformada ou incompatível deve interromper o fluxo antes da abertura em modo w; ela não deve ser silenciosamente substituída por uma coleção vazia. A serialização antecipada reduz um risco de truncamento, mas não desfaz uma falha ocorrida durante a escrita.

Teste os caminhos de sucesso e falha

Quatro execuções manuais

Primeiro, execute com o tarefas.json correto e confirme que o terminal mostra o título acentuado e True. Depois, crie os dois arquivos abaixo e altere temporariamente CAMINHO para cada nome. Para a falha de acesso, use Path("nao_existe.json"). Antes de cada teste, observe o conteúdo do arquivo e confira que entradas inválidas não foram sobrescritas.

malformado.json

Este documento tem uma vírgula final e deve produzir JSONDecodeError, apresentada pelo script como JSON inválido com linha e coluna:

json
{
  "tarefas": [
    {
      "titulo": "Revisar persistência",
      "concluida": false,
      "prioridade": 4,
    }
  ]
}

campo_ausente.json

Este texto é JSON válido, mas não cumpre o contrato da aplicação porque falta concluida:

json
{
  "tarefas": [
    {
      "titulo": "Revisar persistência",
      "prioridade": 4
    }
  ]
}

Registre o que ocorreu

Relate o valor de concluida após a reabertura, a preservação do título acentuado e o diagnóstico obtido com malformado.json, campo_ausente.json e nao_existe.json.

Escreva pelo menos 120 caracteres (0/120).

Revisão final

Explique a proteção antes da gravação

Por que uma entrada inválida não deve provocar a sobrescrita do arquivo original? Inclua na resposta o papel da validação, de json.dumps e do modo w.

Escreva pelo menos 100 caracteres (0/100).

Resumo

Critérios de sucesso

Você concluiu o ciclo de persistência quando consegue carregar, validar, alterar, salvar e reabrir os dados sem confundir as etapas e suas falhas.

  • Use valores compatíveis com JSON e reconheça que nem todo tipo Python preserva suas características na ida e volta.
  • Escolha dumps e loads para strings; use dump e load quando trabalhar diretamente com objetos de arquivo.
  • Abra arquivos de texto com UTF-8 explícito; ensure_ascii controla a representação dos caracteres, não a codificação do arquivo.
  • Mantenha um único documento JSON completo por arquivo, em vez de concatenar documentos.
  • Valide a raiz, os registros, os campos, os tipos e os valores antes de processar ou alterar a coleção.
  • Distinga falha de acesso, decodificação de texto, sintaxe JSON, regras da aplicação e serialização de valores Python.
  • Valide e serialize antes de abrir o destino em modo w, lembrando que isso não protege contra toda falha possível durante a escrita.

Tutorial concluído

Parabéns! Você concluiu: Salvar e carregar dados em JSON

Parabéns! Agora você consegue salvar e carregar coleções em JSON, preservar textos em UTF-8 e diagnosticar separadamente falhas de formato, acesso, estrutura e compatibilidade.

100 XP

Baixe o Aplicativo agora para ter acesso a + de 5000 cursos gratuitos, exercícios, certificado e muito conteúdo sem pagar nada!

  • Cursos online 100% gratuitos do início ao fim

    Milhares de cursos online em vídeo, ebooks e áudiobooks.

  • Mais de 60 mil exercícios gratuitos

    Para testar seus conhecimentos no decorrer dos cursos online

  • Certificado Digital gratuito válido em todo o Brasil

    Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail

Aplicativo Cursa na tela de ebook, na tela de curso em vídeo e na tela de exercícios do curso, mais o certificado de conclusão de curso