Trabalhando com JSON

[104] Trabalhando com JSON

JSON parece um objeto JavaScript, mas é bem mais pobre: não tem data, não tem função, não tem undefined. O artigo vai das regras do formato ao replacer e ao reviver, mostra o que o stringify descarta calado, o problema clássico das datas e por que o clone via JSON perde metade do objeto.
Javascript

21 min de leitura

JSON está em absolutamente tudo. É o formato de dados mais usado na web — em APIs, arquivos de configuração, bancos de dados NoSQL, comunicação entre serviços, LocalStorage e muito mais. Você já o usou nos artigos anteriores (Promises: resolvendo o Callback Hell, Async/Await: escrevendo código assíncrono de forma limpa, Fetch API: consumindo dados da internet), mas de forma superficial.

Neste artigo vamos explorar o JSON com profundidade — suas regras, seus limites, casos especiais, técnicas avançadas e os erros mais comuns que desenvolvedores cometem ao trabalhar com ele.

O que é JSON?

JSONJavaScript Object Notation — é um formato de texto para representar dados estruturados. Criado por Douglas Crockford no início dos anos 2000, foi inspirado na sintaxe de objetos do JavaScript mas é independente de linguagem — praticamente toda linguagem de programação moderna sabe ler e escrever JSON.

{
  "nome": "Ana Paula",
  "idade": 28,
  "ativo": true,
  "endereco": {
    "rua": "Av. Paulista",
    "numero": 1000,
    "cidade": "São Paulo"
  },
  "habilidades": ["JavaScript", "React", "Node.js"],
  "foto": null
}

Regras do JSON — diferenças do objeto JavaScript

JSON parece um objeto JavaScript, mas tem regras mais rígidas:

// ── Objeto JavaScript ──────────────────────────────
const obj = {
  nome: "Ana",            // chave sem aspas — válido em JS
  'idade': 28,            // aspas simples — válido em JS
  ativo: true,
  saudar() {              // métodos — válido em JS
    return "olá";
  },
  indefinido: undefined,  // undefined — válido em JS
};

// ── JSON válido ────────────────────────────────────
// ✅ Chaves SEMPRE entre aspas duplas
// ✅ Strings SEMPRE entre aspas duplas
// ❌ Sem métodos (funções)
// ❌ Sem undefined
// ❌ Sem comentários
// ❌ Sem trailing comma (vírgula no último item)

const jsonValido = `{
  "nome": "Ana",
  "idade": 28,
  "ativo": true,
  "endereco": null
}`;

JSON.stringify() — convertendo para string

const usuario = {
  nome: "Carlos",
  idade: 32,
  email: "carlos@email.com",
  ativo: true,
};

// Básico
const jsonString = JSON.stringify(usuario);
console.log(jsonString);
// {"nome":"Carlos","idade":32,"email":"carlos@email.com","ativo":true}

// Com indentação — muito mais legível
const jsonFormatado = JSON.stringify(usuario, null, 2);
console.log(jsonFormatado);
/*
{
  "nome": "Carlos",
  "idade": 32,
  "email": "carlos@email.com",
  "ativo": true
}
*/

// Com indentação de tab
const jsonTab = JSON.stringify(usuario, null, "\t");

JSON.parse() — convertendo para objeto

const jsonString = '{"nome":"Ana","idade":28,"ativo":true}';

const objeto = JSON.parse(jsonString);

console.log(objeto.nome);   // "Ana"
console.log(objeto.idade);  // 28 — número, não string!
console.log(objeto.ativo);  // true — boolean, não string!
console.log(typeof objeto.idade); // "number"

O que stringify inclui e o que ignora

Esse é um ponto crítico que causa bugs sutis:

const dados = {
  nome: "Pedro",
  idade: 25,
  saudar: function() { return "olá"; },  // ❌ ignorado
  indefinido: undefined,                  // ❌ ignorado
  simbolo: Symbol("id"),                  // ❌ ignorado
  data: new Date(),                       // ⚠️  convertido para string ISO
  regex: /padrão/g,                       // ⚠️  vira objeto vazio {}
  infinito: Infinity,                     // ⚠️  vira null
  naoNumero: NaN,                         // ⚠️  vira null
  nulo: null,                             // ✅ mantido como null
};

console.log(JSON.stringify(dados, null, 2));
/*
{
  "nome": "Pedro",
  "idade": 25,
  "data": "2025-01-15T10:30:00.000Z",
  "regex": {},
  "infinito": null,
  "naoNumero": null,
  "nulo": null
}
*/
// saudar, indefinido e simbolo desapareceram silenciosamente!

O replacer — filtrando e transformando

O segundo argumento do JSON.stringify pode ser uma função ou array para controlar o que é incluído:

const usuario = {
  id: 1,
  nome: "Lucia",
  email: "lucia@email.com",
  senha: "hashDaSenha123",    // não deve ser serializado!
  saldo: 5000,
  createdAt: new Date(),
};

// ── Array replacer — inclui apenas os campos listados
const jsonPublico = JSON.stringify(usuario, ["id", "nome", "email"], 2);
console.log(jsonPublico);
/*
{
  "id": 1,
  "nome": "Lucia",
  "email": "lucia@email.com"
}
*/

// ── Function replacer — controle total
const jsonCustom = JSON.stringify(usuario, (chave, valor) => {
  // Ignora campos sensíveis
  if (chave === "senha") return undefined;

  // Formata datas de forma diferente
  if (valor instanceof Date) {
    return valor.toLocaleDateString("pt-BR");
  }

  // Mascara o saldo
  if (chave === "saldo") {
    return "R$ ***";
  }

  return valor; // retorna normalmente
}, 2);

console.log(jsonCustom);
/*
{
  "id": 1,
  "nome": "Lucia",
  "email": "lucia@email.com",
  "saldo": "R$ ***",
  "createdAt": "15/01/2025"
}
*/

O reviver — transformando ao fazer parse

O segundo argumento do JSON.parse é uma função reviver que transforma valores ao deserializar:

const jsonString = `{
  "nome": "Rafael",
  "nascimento": "1995-03-20T00:00:00.000Z",
  "ultimoAcesso": "2025-01-15T14:30:00.000Z",
  "saldo": "1500.50"
}`;

const objeto = JSON.parse(jsonString, (chave, valor) => {
  // Converte strings de data de volta para objetos Date
  if (chave === "nascimento" || chave === "ultimoAcesso") {
    return new Date(valor);
  }

  // Converte saldo de volta para número
  if (chave === "saldo") {
    return parseFloat(valor);
  }

  return valor;
});

console.log(objeto.nascimento instanceof Date); // true
console.log(objeto.nascimento.getFullYear());   // 1995
console.log(objeto.saldo + 100);                // 1600.5 — número real

O problema das datas com JSON

Datas são o caso mais comum de perda de tipo com JSON:

const evento = {
  titulo: "Conferência JavaScript",
  data: new Date("2025-06-15"),
  duracao: 8 * 60 * 60 * 1000, // 8 horas em ms
};

// Stringify converte Date para string ISO
const json = JSON.stringify(evento);
console.log(json);
// {"titulo":"Conferência JavaScript","data":"2025-06-15T00:00:00.000Z","duracao":28800000}

// Parse não reconstrói o Date automaticamente
const recuperado = JSON.parse(json);
console.log(typeof recuperado.data);           // "string" — não é Date!
console.log(recuperado.data instanceof Date);  // false

// ✅ Soluções:

// 1. Reviver manual
const correto = JSON.parse(json, (chave, valor) => {
  if (chave === "data") return new Date(valor);
  return valor;
});
console.log(correto.data instanceof Date); // true

// 2. Reconstituir após o parse
const obj = JSON.parse(json);
obj.data = new Date(obj.data);

// 3. Guardar timestamp (número) em vez de Date
const eventoSeguro = {
  titulo: "Conferência JavaScript",
  dataTimestamp: new Date("2025-06-15").getTime(), // número — sobrevive ao JSON
};

A armadilha que vem antes do JSON — UTC ou hora local?

Repare na linha acima: new Date("2025-06-15"). Antes de qualquer conversa sobre serialização, essa string já carrega um problema, porque a forma como o Date a interpreta muda conforme ela tenha ou não hora.

// Data PURA (só AAAA-MM-DD) → interpretada como UTC
const a = new Date("2025-06-15");
a.toISOString();                // "2025-06-15T00:00:00.000Z"
a.toLocaleDateString("pt-BR");  // "14/06/2025"  ← um dia antes!

// Data COM hora, sem fuso → interpretada como hora LOCAL
const b = new Date("2025-06-15T00:00");
b.toLocaleDateString("pt-BR");  // "15/06/2025"  ← correto

// As três formas de não errar:
new Date("2025-06-15T00:00:00-03:00"); // fuso explícito na string
new Date(2025, 5, 15);                 // ano, mês BASE ZERO (5 = junho), dia
new Date("2025-06-15T12:00:00");       // meio-dia local: sobrevive a qualquer fuso

No Brasil, em UTC−3, toda data sem hora "volta um dia" ao ser exibida. É provavelmente o bug de data mais comum em aplicações brasileiras, e ele tem o agravante de não aparecer nos testes: servidores de integração contínua costumam rodar em UTC, onde os dois caminhos coincidem. A mesma inconsistência explica por que new Date("05/03/2024") vira 3 de maio — barra é formato americano — e por que getMonth() devolve 5 para junho: o mês é base zero, mas o dia não é.

Cópia profunda com JSON

Um truque popular para fazer deep clone de objetos simples:

const original = {
  nome: "Beatriz",
  endereco: {
    cidade: "Porto Alegre",
    estado: "RS",
  },
  hobbies: ["leitura", "programação"],
};

// ✅ Deep clone simples com JSON
const copia = JSON.parse(JSON.stringify(original));

// Modifica a cópia
copia.endereco.cidade = "Florianópolis";
copia.hobbies.push("ciclismo");

// Original intacto
console.log(original.endereco.cidade); // "Porto Alegre"
console.log(original.hobbies.length);  // 2

Mas esse truque tem limitações — perde funções, datas, undefined, NaN e referências circulares. Para casos complexos, use structuredClone():

// ✅ structuredClone — deep clone nativo e robusto
// Não é ECMAScript: assim como o fetch, vem da especificação HTML
// do WHATWG. Nos navegadores desde 2021–2022, no Node desde a v17.
const copia = structuredClone(original);
// Preserva Dates, RegExp, Map, Set, ArrayBuffer...
// Lança erro em referências circulares

Referências circulares — o erro mais temido

const pessoa = { nome: "João" };
pessoa.amigo = pessoa; // referência circular!

// ❌ Isso lança um erro
JSON.stringify(pessoa);
// TypeError: Converting circular structure to JSON

// ✅ Solução com replacer
function stringifySeguro(obj) {
  const vistos = new WeakSet();

  return JSON.stringify(obj, (chave, valor) => {
    if (typeof valor === "object" && valor !== null) {
      if (vistos.has(valor)) {
        return "[Referência Circular]";
      }
      vistos.add(valor);
    }
    return valor;
  });
}

console.log(stringifySeguro(pessoa));
// {"nome":"João","amigo":"[Referência Circular]"}

JSON5 e outras variantes

O JSON padrão é rígido. Existe o JSON5 — uma extensão não oficial que permite:

// JSON5 — mais permissivo (não é padrão!)
{
  // Comentários são permitidos
  nome: "Ana",           // chaves sem aspas
  'idade': 28,           // aspas simples
  hobbies: [
    "JS",
    "Node",              // trailing comma permitida
  ],
  salario: +5000,        // sinal de mais explícito é permitido
  infinito: Infinity,    // valores especiais
}

JSON5 não é suportado nativamente — precisa de uma biblioteca. Para arquivos de configuração (como tsconfig.json e .eslintrc), algumas ferramentas aceitam comentários — na prática é JSONC (JSON with Comments), não JSON5.

Trabalhando com JSON em APIs reais

Padrões comuns que você vai encontrar:

// ── Resposta típica de uma API REST ────────────────
const respostaAPI = {
  "success": true,
  "data": {
    "usuarios": [
      { "id": 1, "nome": "Ana" },
      { "id": 2, "nome": "Bruno" },
    ],
    "total": 2,
    "pagina": 1,
    "porPagina": 10
  },
  "meta": {
    "versao": "1.0",
    "timestamp": "2025-01-15T10:30:00Z"
  }
};

// Extraindo dados com desestruturação
const { data: { usuarios, total, pagina } } = respostaAPI;
console.log(`${total} usuários — página ${pagina}`);
usuarios.forEach(u => console.log(u.nome));

// ── Paginação ─────────────────────────────────────
async function buscarTodosUsuarios() {
  const todos = [];
  let pagina = 1;
  let temMais = true;

  while (temMais) {
    const response = await fetch(
      `https://api.exemplo.com/usuarios?pagina=${pagina}&por_pagina=10`
    );
    const { data } = await response.json();

    todos.push(...data.usuarios);

    temMais = data.usuarios.length === 10; // se veio menos, acabou
    pagina++;
  }

  return todos;
}

Validação de JSON recebido

Nunca confie cegamente em dados de uma API externa:

function validarUsuario(dados) {
  const erros = [];

  if (!dados || typeof dados !== "object") {
    throw new Error("Dados inválidos — esperado um objeto.");
  }

  if (!dados.id || typeof dados.id !== "number") {
    erros.push("id deve ser um número.");
  }

  if (!dados.nome || typeof dados.nome !== "string" || dados.nome.trim().length < 2) {
    erros.push("nome deve ser uma string com pelo menos 2 caracteres.");
  }

  if (!dados.email || !dados.email.includes("@")) {
    erros.push("email inválido.");
  }

  if (erros.length > 0) {
    throw new Error(`Dados inválidos:
- ${erros.join("\n- ")}`);
  }

  return true;
}

async function buscarEValidar(id) {
  const response = await fetch(`https://api.exemplo.com/usuarios/${id}`);
  const dados = await response.json();

  try {
    validarUsuario(dados);
    return dados;
  } catch (erro) {
    console.error(`Resposta inválida da API: ${erro.message}`);
    throw erro;
  }
}

Utilitário completo para JSON

const Json = {
  // Parse seguro com valor padrão
  parseSafe(texto, padrao = null) {
    try {
      return JSON.parse(texto);
    } catch {
      return padrao;
    }
  },

  // Stringify com tratamento de circulares
  stringifySafe(obj, espacos = 0) {
    const vistos = new WeakSet();
    return JSON.stringify(obj, (chave, valor) => {
      if (typeof valor === "object" && valor !== null) {
        if (vistos.has(valor)) return "[Circular]";
        vistos.add(valor);
      }
      return valor;
    }, espacos);
  },

  // Deep clone
  clonar(obj) {
    return this.parseSafe(JSON.stringify(obj));
  },

  // Verificar se uma string é JSON válido
  eValido(texto) {
    try {
      JSON.parse(texto);
      return true;
    } catch {
      return false;
    }
  },

  // Mescla RASA: as chaves do último objeto vencem, e um objeto
  // aninhado é substituído inteiro, nunca combinado com o anterior.
  // O JSON aqui serve só para clonar cada entrada antes de espalhar.
  mesclar(...objetos) {
    return objetos.reduce((acc, obj) => ({
      ...acc,
      ...JSON.parse(JSON.stringify(obj)),
    }), {});
  },
};

// Uso
console.log(Json.eValido('{"nome": "Ana"}')); // true
console.log(Json.eValido("isso não é json")); // false

const clone = Json.clonar({ a: { b: { c: 42 } } });
console.log(clone.a.b.c); // 42

const seguro = Json.stringifySafe({ nome: "João", ref: null });
console.log(seguro); // {"nome":"João","ref":null}

Tarefa para você

Construa um sistema de exportação e importação de dados:

// 1. Crie um array de 5 produtos com:
//    { id, nome, preco, categoria, criadoEm: new Date(), ativo }

// 2. Exporte para JSON formatado, mas:
//    - Formata datas como "DD/MM/AAAA"
//    - Remove produtos inativos
//    - Mascara preços acima de R$1000 como "Preço sob consulta"

// 3. Importe de volta do JSON e:
//    - Reconstrói as datas como objetos Date
//    - Valida que cada produto tem id, nome e preco
//    - Lança erro descritivo se algum campo estiver faltando

// 4. Exiba um relatório:
//    - Total de produtos exportados/importados
//    - Categorias únicas encontradas
//    - Produto mais caro
//    - Data de criação mais antiga
Ver solução — exportar e importar com replacer e reviver, sem perder as datas
// ---------------------------------------------------------------
// 1 — os produtos
// ---------------------------------------------------------------
const produtos = [
  { id: 1, nome: "Notebook Pro", preco: 7499.9, categoria: "Eletrônicos", criadoEm: new Date("2024-03-12T14:30:00"), ativo: true },
  { id: 2, nome: "Mouse sem fio", preco: 149.9, categoria: "Periféricos", criadoEm: new Date("2024-05-02T09:15:00"), ativo: true },
  { id: 3, nome: "Cadeira ergonômica", preco: 1890, categoria: "Móveis", criadoEm: new Date("2023-11-20T16:45:00"), ativo: true },
  { id: 4, nome: "Teclado mecânico", preco: 599, categoria: "Periféricos", criadoEm: new Date("2024-01-08T11:00:00"), ativo: false },
  { id: 5, nome: "Monitor 27\"", preco: 2299, categoria: "Eletrônicos", criadoEm: new Date("2024-06-30T08:20:00"), ativo: true },
];

const LIMITE_SOB_CONSULTA = 1000;

// ---------------------------------------------------------------
// 2 — exportar
// ---------------------------------------------------------------
const paraBR = (data) =>
  data.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric" });

function exportar(lista) {
  // O filtro vem ANTES do stringify: o replacer não consegue remover um
  // item de array (devolver undefined ali vira `null` no JSON, deixando
  // buracos na lista).
  const ativos = lista.filter((produto) => produto.ativo);

  return JSON.stringify(
    ativos,
    // O replacer recebe (chave, valor) para cada nó. Cuidado: o `valor`
    // de uma data JÁ CHEGA como string ISO, porque o JSON.stringify
    // chama toJSON() antes de passar aqui. Por isso a conversão usa
    // `this[chave]`, que ainda é o objeto Date original.
    function (chave, valor) {
      if (chave === "criadoEm") return paraBR(this[chave]);

      if (chave === "preco" && valor > LIMITE_SOB_CONSULTA) {
        return "Preço sob consulta";
      }

      return valor;
    },
    2
  );
}

const json = exportar(produtos);
console.log(json);

// ---------------------------------------------------------------
// 3 — importar
// ---------------------------------------------------------------
class ErroDeImportacao extends Error {
  constructor(mensagem, indice, campo) {
    super(mensagem);
    this.name = "ErroDeImportacao";
    this.indice = indice;
    this.campo = campo;
  }
}

const BR = /^(\d{2})\/(\d{2})\/(\d{4})$/;

function importar(texto) {
  const bruto = JSON.parse(texto, (chave, valor) => {
    // O reviver é o caminho de volta do replacer: aqui a string vira
    // Date de novo. new Date("05/03/2024") não serve — o construtor lê
    // no formato americano e entenderia 3 de maio.
    if (chave === "criadoEm" && typeof valor === "string") {
      const partes = BR.exec(valor);
      if (partes) {
        const [, dia, mes, ano] = partes;
        return new Date(Number(ano), Number(mes) - 1, Number(dia));
      }
    }

    return valor;
  });

  bruto.forEach((produto, indice) => {
    for (const campo of ["id", "nome", "preco"]) {
      if (produto[campo] === undefined || produto[campo] === null) {
        throw new ErroDeImportacao(
          `Produto na posição ${indice} está sem o campo obrigatório "${campo}".`,
          indice,
          campo
        );
      }
    }

    if (typeof produto.nome !== "string" || produto.nome.trim() === "") {
      throw new ErroDeImportacao(
        `Produto ${produto.id}: "nome" precisa ser texto não vazio.`,
        indice,
        "nome"
      );
    }

    // O preço mascarado volta como string. Isso é dado perdido, não
    // erro de formato — ver a nota no fim do bloco.
    if (typeof produto.preco !== "number" && produto.preco !== "Preço sob consulta") {
      throw new ErroDeImportacao(
        `Produto ${produto.id}: "preco" inválido (${JSON.stringify(produto.preco)}).`,
        indice,
        "preco"
      );
    }
  });

  return bruto;
}

const importados = importar(json);

console.log(importados[0].criadoEm instanceof Date); // true
console.log(importados[0].criadoEm.getFullYear());   // 2024

// Um erro de verdade, para ver a mensagem:
try {
  importar('[{"id": 9, "preco": 10}]');
} catch (erro) {
  console.error(`${erro.name}: ${erro.message}`);
  // ErroDeImportacao: Produto na posição 0 está sem o campo obrigatório "nome".
}

// ---------------------------------------------------------------
// 4 — relatório
// ---------------------------------------------------------------
function relatorio(originais, exportados, importados) {
  const comPreco = importados.filter((p) => typeof p.preco === "number");

  // Atenção ao que este número significa: é o mais caro ENTRE OS QUE
  // ainda têm preço. Como a exportação mascara tudo acima de mil, os
  // três produtos realmente caros viraram texto e ficaram de fora — o
  // "mais caro" que sobra é um mouse de R$ 149,90. O relatório está
  // certo; o dado é que foi destruído na exportação.
  const maisCaro = comPreco.length
    ? comPreco.reduce((maior, p) => (p.preco > maior.preco ? p : maior))
    : null;

  const maisAntiga = importados
    .map((p) => p.criadoEm)
    .filter((d) => d instanceof Date)
    .reduce((menor, d) => (d < menor ? d : menor));

  return {
    exportados: exportados.length,
    importados: importados.length,
    descartados: originais.length - exportados.length,
    categorias: [...new Set(importados.map((p) => p.categoria))],
    maisCaroComPreco: maisCaro ? `${maisCaro.nome} (${maisCaro.preco})` : "todos sob consulta",
    precosMascarados: importados.length - comPreco.length,
    maisAntiga: paraBR(maisAntiga),
  };
}

console.log(relatorio(produtos, JSON.parse(json), importados));
// maisCaroComPreco: 'Mouse sem fio (149.9)' · precosMascarados: 3
//
// O nome do campo é `maisCaroComPreco`, e não `maisCaro`, porque a
// segunda coisa seria mentira: o produto mais caro de verdade é o
// notebook de R$ 7.499,90, que virou "Preço sob consulta" e não pode
// mais ser comparado. Relatório sobre dado mascarado responde outra
// pergunta — o nome do campo tem de dizer qual.

// ---------------------------------------------------------------
// O detalhe que morde: JSON não tem tipos, só quatro primitivos
// ---------------------------------------------------------------
const original = {
  data: new Date(),
  indefinido: undefined,
  funcao: () => 1,
  naoNumero: NaN,
  infinito: Infinity,
  conjunto: new Set([1, 2]),
  grande: 10n,
};

console.log(JSON.parse(JSON.stringify({ ...original, grande: undefined })));
// { data: '2026-08-17T...' (string!), naoNumero: null, infinito: null, conjunto: {} }
//
// undefined e função SOMEM do objeto. Date vira string. NaN e Infinity
// viram null. Set e Map viram {} — sem erro, sem aviso.
// E BigInt é o único que reclama:
try {
  JSON.stringify({ grande: 10n });
} catch (erro) {
  console.error(erro.message); // Do not know how to serialize a BigInt
}

O replacer recebe a data já convertida em string ISO, porque JSON.stringify chama toJSON() antes — para pegar o Date original é preciso this[chave]. E repare no que a máscara de preço custa: "Preço sob consulta" é uma via de mão única, o número não volta na importação. Mascarar na exibição preserva o dado; mascarar na exportação o destrói.

Sem data, sem função, sem undefined, sem NaN, sem Infinity, sem referência circular — a lista do que o JSON não tem é mais útil de guardar do que a do que ele tem. Ela explica de uma vez por que o clone com JSON.parse(JSON.stringify(x)) devolve um objeto empobrecido, por que toda data volta como string, e por que o structuredClone é a escolha certa quando a cópia precisa ser fiel ao original.

Fontes e Referências

Exercícios

Exercício 1

Quais chaves sobrevivem ao JSON.stringify, e qual delas faz o método lançar erro?

const dados = {
  texto: "ok",
  vazio: undefined,
  metodo() { return 1; },
  naoNumero: NaN,
  conjunto: new Set([1, 2]),
  nulo: null,
  grande: 10n,
};

console.log(JSON.stringify(dados));
Ver resposta

✓ Resposta: Lança TypeError: Do not know how to serialize a BigInt por causa de grande — o único tipo que o stringify se recusa a converter em vez de improvisar. Removendo essa chave, o resultado é {"texto":"ok","naoNumero":null,"conjunto":{},"nulo":null}, e cada ausência tem um motivo diferente. vazio e metodo somem, porque undefined e função não têm representação em JSON e o algoritmo simplesmente omite a propriedade. NaN vira null, assim como Infinity — o JSON não tem esses valores. O Set vira {}, o mais cruel dos casos: ele é um objeto, não tem propriedades próprias enumeráveis, e o resultado é um objeto vazio sem erro nem aviso, com os dois números perdidos para sempre. E há um detalhe de comportamento que muda conforme o contexto: dentro de um array, undefined e função não somem — viram null, porque um array não pode ter buracos e manter os índices corretos.

Exercício 2

O replacer quer formatar a data em português. Por que ele não funciona, e o que valor contém quando a chave é criadoEm?

const registro = { titulo: "Evento", criadoEm: new Date("2024-03-05T12:00:00Z") };

JSON.stringify(registro, (chave, valor) => {
  if (valor instanceof Date) return valor.toLocaleDateString("pt-BR");
  return valor;
});
Ver resposta

✓ Resposta: O if nunca é verdadeiro, e a data sai no formato ISO de sempre. Quando o replacer é chamado, valor já é a string "2024-03-05T12:00:00.000Z", não um Date — porque o JSON.stringify primeiro procura um método toJSON() no valor e, encontrando, usa o retorno dele; Date.prototype.toJSON existe e devolve o ISO. Só depois disso o replacer recebe o resultado. Como "..." instanceof Date é false, o teste falha em silêncio. A saída é usar o this do replacer, que é o objeto dono da chave e ainda guarda o valor original: if (this[chave] instanceof Date) return this[chave].toLocaleDateString("pt-BR") — e para isso o replacer precisa ser uma function, nunca uma arrow, que não tem this próprio. A outra saída, mais direta, é converter as datas antes de chamar o stringify.

Exercício 3

Este é o truque de deep clone mais conhecido do JavaScript. O que a cópia perde?

const original = {
  nome: "Ana",
  criadoEm: new Date(),
  tags: new Set(["a", "b"]),
  apelido: undefined,
  nota: NaN,
};

const copia = JSON.parse(JSON.stringify(original));
Ver resposta

✓ Resposta: Perde quase tudo que não seja texto, número, booleano, array ou objeto simples. criadoEm volta como string, e a partir daí copia.criadoEm.getFullYear() lança TypeError. tags vira {}, um objeto vazio. apelido desaparece do objeto — e note a diferença sutil: a chave deixa de existir, então "apelido" in copia passa a ser false, o que quebra qualquer código que distinga "não informado" de "ausente". nota vira null. Some-se a isso que uma referência circular faz o stringify lançar erro, e que objetos grandes pagam o custo de virar texto e ser reanalisados. O substituto correto é structuredClone(original), que preserva Date, Map, Set, RegExp, ArrayBuffer e até referências circulares — a limitação dele é o oposto: lança erro em funções, em vez de descartá-las em silêncio, o que quase sempre é melhor.

Exercício 4

A API devolveu um usuário legítimo cujo id é 0. O que esta validação faz?

if (!dados.id || typeof dados.id !== "number") {
  erros.push("id deve ser um número.");
}
Ver resposta

✓ Resposta: Rejeita o usuário. !dados.id é verdadeiro para 0, porque zero é falsy — e a segunda metade da condição, que estava certa, nem chega a ser avaliada, já que o || curto-circuita no primeiro termo verdadeiro. É a mesma armadilha do || contra o ?? vista no artigo da calculadora, aqui disfarçada de validação. O mesmo defeito atinge string vazia e false: um campo ativo: false validado com if (!dados.ativo) seria acusado de ausente. A intenção real era distinguir ausente de presente com valor inválido, e para isso existem testes precisos: dados.id === undefined para ausência, !("id" in dados) quando a chave em si importa, e typeof dados.id !== "number" || !Number.isInteger(dados.id) para o formato. Vale lembrar que typeof NaN === "number", então validar número sem checar Number.isFinite deixa passar o pior dos números.

Exercício 5

O trecho abaixo é a paginação do artigo. Quantas requisições ela faz quando existem exatamente 30 usuários? E quando existem 25?

let pagina = 1;
let temMais = true;

while (temMais) {
  const response = await fetch(`/api/usuarios?pagina=${pagina}&por_pagina=10`);
  const { data } = await response.json();

  todos.push(...data.usuarios);

  temMais = data.usuarios.length === 10;
  pagina++;
}
Ver resposta

✓ Resposta: Com 30 usuários faz quatro requisições; com 25, faz três. No caso dos 30, as três primeiras trazem dez cada e a condição continua verdadeira; a quarta volta vazia, a condição fica falsa e o laço para. Ou seja, quando o total é múltiplo exato do tamanho da página, há sempre uma ida desperdiçada ao servidor — inofensiva, mas evitável se a API informar o total e o laço comparar todos.length com ele. Com 25, a terceira página traz cinco itens, a condição já falha e o laço encerra em três. O que merece mais atenção é o que esse código faz de bom: ele é sequencial de propósito. Não dá para paralelizar as páginas sem saber quantas existem, e disparar dez requisições "por garantia" é justamente o que faz uma API responder 429. Quando o servidor informa o total, aí sim a primeira requisição pode revelar quantas páginas há e as demais irem juntas com Promise.all — de preferência com um limite de quantas correm ao mesmo tempo.

Comentários

Mais em Javascript

Projeto Final: Revisão Completa e Aplicação de Produção
Projeto Final: Revisão Completa e Aplicação de Produção

O fim da série reúne tudo numa aplicação de produção: React com rotas e…

Revisão + Projeto Final: SPA Completa
Revisão + Projeto Final: SPA Completa

As cinco peças do módulo em uma aplicação só: rotas com layout e proteção…

Performance em aplicações web
Performance em aplicações web

É fácil otimizar a coisa errada, e por isso a ordem importa: medir antes. O…