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?
JSON — JavaScript 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
- MDN Web Docs — JSON: https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Global_Objects/JSON
- MDN Web Docs — JSON.stringify: https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
- MDN Web Docs — JSON.parse: https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
- MDN Web Docs — structuredClone: https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
- JavaScript.info — JSON methods: https://javascript.info/json
- JSON5 Spec: https://json5.org
- Douglas Crockford — Introducing JSON: https://www.json.org/json-en.html
- JavaScript: The Good Parts — Douglas Crockford (O'Reilly)
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.