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, 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, " ");
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
};
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 (ES2022)
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: 5_000, // underscore em números
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("
- ")}`);
}
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;
}
},
// Mesclar objetos profundamente via JSON (para objetos simples)
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
Conclusão
Neste artigo você aprendeu:
- As regras rígidas do JSON e como diferem do objeto JavaScript
JSON.stringify()com replacer e indentaçãoJSON.parse()com reviver para reconstituir tipos- O que stringify ignora silenciosamente — a armadilha das funções e
undefined - O problema clássico das datas com JSON
- Deep clone com JSON e a alternativa moderna
structuredClone - Como lidar com referências circulares
- Paginação e validação de respostas de APIs
- Um utilitário completo e reutilizável para JSON
No próximo artigo vamos aprender sobre tratamento de erros em requisições — como lidar com timeouts, falhas de rede, erros de API e criar uma estratégia robusta de resiliência.
📌 Próximo artigo: Aula 24 — Tratamento de erros em requisições HTTP
📚 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)
- You Don't Know JS: Types & Grammar — Kyle Simpson: https://github.com/getify/You-Dont-Know-JS
Prof. Ricardo Matos
Mestre em Programação | Série: Dominando JavaScript em 1 Ano — Artigo 23/52
Quer a Aula 24 — Tratamento de erros em requisições HTTP agora?
# you asked
Vai um B e mostre como trabablhar com .csv também por favor. Acho interessante abordar isso, aí vamos para a 24
# claude response
# 📘 Série: Dominando o JavaScript em 1 Ano
Artigo 23-B — Trabalhando com CSV no JavaScript
Por Prof. Ricardo Matos | Mestre em Programação
Módulo 3 — JavaScript Assíncrono e APIs
Introdução
JSON domina as APIs modernas, mas o CSV domina o mundo real dos dados. Planilhas financeiras, exportações de bancos de dados, relatórios de e-commerce, dados de sensores, listas de contatos — tudo isso vive em CSV. Todo desenvolvedor, cedo ou tarde, precisa ler, processar e gerar arquivos .csv.
Neste artigo vamos aprender a trabalhar com CSV do zero, sem bibliotecas externas, e depois ver quando e como usar uma biblioteca para casos complexos.
O que é CSV?
CSV — Comma-Separated Values — é um formato de texto simples onde cada linha representa um registro e os campos são separados por vírgulas (ou outro delimitador):
nome,email,idade,cidade
Ana Paula,ana@email.com,28,São Paulo
Carlos Silva,carlos@email.com,32,Curitiba
Beatriz Costa,bia@email.com,25,Rio de Janeiro
Simples assim. A primeira linha normalmente é o cabeçalho com os nomes das colunas.
Lendo CSV manualmente — o básico
const csvTexto = `nome,email,idade,cidade
Ana Paula,ana@email.com,28,São Paulo
Carlos Silva,carlos@email.com,32,Curitiba
Beatriz Costa,bia@email.com,25,Rio de Janeiro`;
function parsearCSV(texto) {
const linhas = texto.trim().split("
");
const cabecalho = linhas[0].split(",");
return linhas.slice(1).map(linha => {
const valores = linha.split(",");
return cabecalho.reduce((obj, col, i) => {
obj[col.trim()] = valores[i]?.trim();
return obj;
}, {});
});
}
const dados = parsearCSV(csvTexto);
console.log(dados);
/*
[
{ nome: "Ana Paula", email: "ana@email.com", idade: "28", cidade: "São Paulo" },
{ nome: "Carlos Silva", email: "carlos@email.com", idade: "32", cidade: "Curitiba" },
{ nome: "Beatriz Costa", email: "bia@email.com", idade: "25", cidade: "Rio de Janeiro" }
]
*/
Funciona para casos simples. Mas o mundo real é mais complicado.
O problema das vírgulas dentro dos campos
O CSV tem uma armadilha clássica: e se o campo contiver uma vírgula? A solução padrão é envolver o campo em aspas duplas:
nome,descricao,preco
Notebook Pro,"Processador i7, 16GB RAM, SSD 512GB",3500.00
Mouse Gamer,"Clique preciso, 6 botões",189.90
E se o campo contiver aspas? Elas são escapadas dobrando: "":
produto,observacao
Caneta,"Cor ""azul"" especial"
Isso torna o parse manual muito mais complexo:
function parsearCSVRobusto(texto, delimitador = ",") {
const linhas = texto.trim().split("
");
const cabecalho = parsearLinha(linhas[0], delimitador);
return linhas.slice(1).map(linha => {
const valores = parsearLinha(linha, delimitador);
return cabecalho.reduce((obj, col, i) => {
obj[col.trim()] = valores[i] ?? "";
return obj;
}, {});
});
}
function parsearLinha(linha, delimitador = ",") {
const resultado = [];
let campo = "";
let dentroDeAspas = false;
for (let i = 0; i < linha.length; i++) {
const char = linha[i];
const proximo = linha[i + 1];
if (char === '"' && !dentroDeAspas) {
dentroDeAspas = true;
continue;
}
if (char === '"' && dentroDeAspas) {
if (proximo === '"') {
// Aspas escapadas "" → uma aspas literal
campo += '"';
i++; // pula o próximo "
} else {
dentroDeAspas = false;
}
continue;
}
if (char === delimitador && !dentroDeAspas) {
resultado.push(campo.trim());
campo = "";
continue;
}
campo += char;
}
resultado.push(campo.trim()); // último campo
return resultado;
}
// Testando
const csv = `produto,descricao,preco
Notebook,"Processador i7, 16GB RAM",3500.00
Caneta,"Cor ""azul"" especial",2.50
Mouse Gamer,"6 botões, RGB",189.90`;
const dados = parsearCSVRobusto(csv);
console.log(dados[0].descricao); // "Processador i7, 16GB RAM"
console.log(dados[1].descricao); // 'Cor "azul" especial'
Convertendo tipos automaticamente
O CSV é sempre texto puro. Você precisa converter tipos manualmente:
function converterTipos(obj) {
const resultado = {};
for (const [chave, valor] of Object.entries(obj)) {
// Vazio → null
if (valor === "" || valor === "null" || valor === "NULL") {
resultado[chave] = null;
continue;
}
// Boolean
if (valor === "true" || valor === "TRUE") { resultado[chave] = true; continue; }
if (valor === "false" || valor === "FALSE") { resultado[chave] = false; continue; }
// Número
const numero = Number(valor);
if (!isNaN(numero) && valor !== "") {
resultado[chave] = numero;
continue;
}
// Data ISO
const data = new Date(valor);
if (!isNaN(data.getTime()) && valor.includes("-")) {
resultado[chave] = data;
continue;
}
// String simples
resultado[chave] = valor;
}
return resultado;
}
// Uso junto com o parser
function parsearCSVComTipos(texto, delimitador = ",") {
return parsearCSVRobusto(texto, delimitador).map(converterTipos);
}
const csv = `nome,idade,ativo,salario,admissao
Ana,28,true,5000.50,2022-03-15
Bruno,35,false,7200,2019-08-01`;
const dados = parsearCSVComTipos(csv);
console.log(dados[0].idade + 1); // 29 — número
console.log(dados[0].ativo === true); // true — boolean
console.log(dados[0].salario + 500); // 5500.5 — número
console.log(dados[0].admissao.getFullYear()); // 2022 — Date
Gerando CSV a partir de dados
function gerarCSV(dados, opcoes = {}) {
const {
delimitador = ",",
incluirCabecalho = true,
campos = null, // null = todos os campos
} = opcoes;
if (dados.length === 0) return "";
const colunas = campos || Object.keys(dados[0]);
function escaparCampo(valor) {
if (valor === null || valor === undefined) return "";
const texto = valor instanceof Date
? valor.toISOString()
: String(valor);
// Envolve em aspas se contém delimitador, aspas ou quebra de linha
if (
texto.includes(delimitador) ||
texto.includes('"') ||
texto.includes("
")
) {
return `"${texto.replace(/"/g, '""')}"`;
}
return texto;
}
const linhas = [];
if (incluirCabecalho) {
linhas.push(colunas.map(escaparCampo).join(delimitador));
}
dados.forEach(item => {
const linha = colunas.map(col => escaparCampo(item[col]));
linhas.push(linha.join(delimitador));
});
return linhas.join("
");
}
// Testando
const funcionarios = [
{ nome: "Ana", cargo: "Dev", salario: 5000, ativo: true },
{ nome: "Bruno, Jr.", cargo: "Designer", salario: 4000, ativo: false },
{ nome: 'Clara "CB"', cargo: "QA", salario: 4500, ativo: true },
];
const csv = gerarCSV(funcionarios);
console.log(csv);
/*
nome,cargo,salario,ativo
Ana,Dev,5000,true
"Bruno, Jr.",Designer,4000,false
"Clara ""CB""",QA,4500,true
*/
Exportando CSV para download no navegador
function baixarCSV(dados, nomeArquivo = "exportacao.csv", opcoes = {}) {
const csvTexto = gerarCSV(dados, opcoes);
// BOM para compatibilidade com Excel no Windows (UTF-8)
const bom = "uFEFF";
const blob = new Blob([bom + csvTexto], {
type: "text/csv;charset=utf-8;",
});
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = nomeArquivo;
link.style.display = "none";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Libera a URL da memória
setTimeout(() => URL.revokeObjectURL(url), 100);
}
// Uso
const vendas = [
{ mes: "Janeiro", receita: 48000, pedidos: 320, ticket: 150 },
{ mes: "Fevereiro", receita: 52000, pedidos: 347, ticket: 149.9 },
{ mes: "Março", receita: 61000, pedidos: 410, ticket: 148.8 },
];
document.querySelector("#btn-exportar").addEventListener("click", () => {
baixarCSV(vendas, "relatorio-vendas-2025.csv");
});
Lendo CSV enviado pelo usuário
function lerArquivoCSV(arquivo) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (evento) => {
const texto = evento.target.result;
try {
const dados = parsearCSVComTipos(texto);
resolve(dados);
} catch (erro) {
reject(new Error(`Erro ao parsear CSV: ${erro.message}`));
}
};
reader.onerror = () => reject(new Error("Erro ao ler o arquivo."));
reader.readAsText(arquivo, "UTF-8");
});
}
// No HTML: <input type="file" id="arquivo-csv" accept=".csv">
const inputArquivo = document.querySelector("#arquivo-csv");
inputArquivo.addEventListener("change", async (e) => {
const arquivo = e.target.files[0];
if (!arquivo) return;
if (!arquivo.name.endsWith(".csv")) {
console.error("Por favor, selecione um arquivo .csv");
return;
}
try {
const dados = await lerArquivoCSV(arquivo);
console.log(`${dados.length} registros carregados.`);
console.log("Primeiro registro:", dados[0]);
processarDados(dados);
} catch (erro) {
console.error(erro.message);
}
});
Buscando CSV de uma URL com Fetch
async function buscarCSV(url, opcoes = {}) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Erro ${response.status} ao buscar CSV.`);
}
const texto = await response.text();
return parsearCSVComTipos(texto, opcoes.delimitador);
}
// Exemplo com dados públicos do IBGE (ou qualquer CSV público)
async function carregarDadosPublicos() {
try {
const dados = await buscarCSV(
"https://raw.githubusercontent.com/datasets/population/main/data/population.csv"
);
console.log(`${dados.length} registros carregados.`);
// Filtra só o Brasil
const brasil = dados.filter(d =>
d["Country Name"] === "Brazil" || d["Country Code"] === "BRA"
);
console.log("Dados do Brasil:", brasil);
} catch (erro) {
console.error("Erro:", erro.message);
}
}
Delimitadores alternativos — TSV e ponto e vírgula
Nem todo "CSV" usa vírgula. O Excel brasileiro gera com ponto e vírgula por padrão (pois a vírgula é separador decimal no Brasil). Arquivos TSV usam tab:
// Detectar delimitador automaticamente
function detectarDelimitador(primeiraLinha) {
const delimitadores = [",", ";", " ", "|"];
const contagens = delimitadores.map(d => ({
delimitador: d,
contagem: (primeiraLinha.match(new RegExp(`\${d}`, "g")) || []).length,
}));
return contagens.sort((a, b) => b.contagem - a.contagem)[0].delimitador;
}
function parsearCSVAuto(texto) {
const primeiraLinha = texto.split("
")[0];
const delimitador = detectarDelimitador(primeiraLinha);
console.log(`Delimitador detectado: "${delimitador === " " ? "TAB" : delimitador}"`);
return parsearCSVRobusto(texto, delimitador);
}
// Funciona com qualquer um:
parsearCSVAuto("nome;email;idade
Ana;ana@email.com;28"); // ponto e vírgula
parsearCSVAuto("nome email idade
Ana ana@email.com 28"); // tab
parsearCSVAuto("nome,email,idade
Ana,ana@email.com,28"); // vírgula
Quando usar uma biblioteca — PapaParse
Para casos complexos em produção, a biblioteca PapaParse é o padrão da indústria:
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script>
// PapaParse — simples, robusto, rápido
// Parse de string
const resultado = Papa.parse(csvTexto, {
header: true, // usa primeira linha como chaves
dynamicTyping: true, // converte tipos automaticamente
skipEmptyLines: true, // ignora linhas vazias
delimiter: "", // auto-detecta
});
console.log(resultado.data); // array de objetos
console.log(resultado.errors); // erros encontrados
console.log(resultado.meta); // delimitador usado, encoding, etc.
// Parse de arquivo (streaming — bom para arquivos grandes)
Papa.parse(arquivo, {
header: true,
dynamicTyping: true,
step: (resultado) => {
// Processa linha por linha — não carrega tudo na memória
processarLinha(resultado.data);
},
complete: (resultado) => {
console.log(`${resultado.data.length} linhas processadas.`);
},
error: (erro) => {
console.error("Erro:", erro.message);
},
});
// Parse de URL remota
Papa.parse("https://dados.exemplo.com/relatorio.csv", {
download: true,
header: true,
dynamicTyping: true,
complete: (resultado) => {
console.log(resultado.data);
},
});
// Gerar CSV
const csv = Papa.unparse(dados, {
quotes: true, // sempre envolve campos em aspas
delimiter: ";", // ponto e vírgula para Excel BR
newline: "
", // Windows line ending
});
JSON vs CSV — quando usar cada um
| Critério | JSON | CSV |
|----------|------|-----|
| Estrutura aninhada | ✅ Suporta objetos/arrays | ❌ Apenas tabelas planas |
| Tipos de dado | ✅ Preserva string, number, boolean, null | ⚠️ Tudo é string |
| Legibilidade | ✅ Clara e auto-descritiva | ✅ Simples para tabelas |
| Tamanho | ⚠️ Maior (chaves repetidas) | ✅ Compacto |
| Excel / Planilhas | ❌ Não abre diretamente | ✅ Abre com duplo clique |
| APIs REST | ✅ Padrão da indústria | ⚠️ Raro |
| Exportação de relatórios | ⚠️ Necessita processamento | ✅ Ideal |
| Big Data / Analytics | ⚠️ Pesado para volumes grandes | ✅ Eficiente |
| Configuração | ✅ Ideal | ❌ Não faz sentido |
Exemplo completo — importador de contatos
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<title>Importador de Contatos CSV</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', sans-serif;
background: #f0f2f5;
padding: 2rem 1rem;
}
.app {
max-width: 700px;
margin: 0 auto;
background: white;
border-radius: 12px;
padding: 2rem;
box-shadow: 0 4px 24px rgba(0,0,0,.08);
}
h1 { margin-bottom: 1.5rem; font-size: 1.4rem; }
.zona-drop {
border: 2px dashed #d0d5dd;
border-radius: 10px;
padding: 2.5rem;
text-align: center;
cursor: pointer;
transition: all .2s;
margin-bottom: 1.5rem;
color: #667085;
}
.zona-drop:hover, .zona-drop.arrastando {
border-color: #5c6bc0;
background: #f5f4ff;
color: #5c6bc0;
}
.zona-drop input { display: none; }
.zona-drop .icone { font-size: 2.5rem; margin-bottom: .5rem; }
.acoes {
display: flex;
gap: .75rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
button {
padding: .6rem 1.25rem;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: .9rem;
font-weight: 600;
transition: all .2s;
}
.btn-primary { background: #5c6bc0; color: white; }
.btn-primary:hover { background: #3949ab; }
.btn-secondary { background: #f2f4f7; color: #344054; }
.btn-secondary:hover { background: #e4e7ec; }
.btn-danger { background: #fef3f2; color: #b42318; }
.stats {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.stat {
background: #f9fafb;
border: 1px solid #eaecf0;
border-radius: 8px;
padding: .75rem 1.25rem;
font-size: .85rem;
}
.stat strong { display: block; font-size: 1.4rem; color: #5c6bc0; }
table {
width: 100%;
border-collapse: collapse;
font-size: .9rem;
}
th {
background: #f9fafb;
padding: .65rem 1rem;
text-align: left;
font-size: .8rem;
color: #667085;
text-transform: uppercase;
letter-spacing: .05em;
border-bottom: 1px solid #eaecf0;
}
td {
padding: .65rem 1rem;
border-bottom: 1px solid #f2f4f7;
color: #344054;
}
tr:last-child td { border: none; }
tr:hover td { background: #f9fafb; }
.badge {
display: inline-block;
padding: .15rem .6rem;
border-radius: 999px;
font-size: .75rem;
font-weight: 600;
}
.badge-sim { background: #ecfdf3; color: #027a48; }
.badge-nao { background: #fef3f2; color: #b42318; }
.vazio { text-align: center; color: #98a2b3; padding: 2rem; }
.erro { background: #fef3f2; border: 1px solid #fecdca;
border-radius: 8px; padding: 1rem; color: #b42318;
margin-bottom: 1rem; display: none; }
.erro.visivel { display: block; }
</style>
</head>
<body>
<div class="app">
<h1>📋 Importador de Contatos</h1>
<div class="zona-drop" id="zona-drop">
<div class="icone">📂</div>
<p><strong>Arraste um arquivo CSV aqui</strong></p>
<p style="font-size:.85rem; margin-top:.4rem">ou clique para selecionar</p>
<input type="file" id="input-arquivo" accept=".csv,.tsv,.txt">
</div>
<div class="erro" id="erro"></div>
<div class="acoes">
<button class="btn-primary" id="btn-exportar" disabled>⬇ Exportar CSV</button>
<button class="btn-secondary" id="btn-exemplo">📄 Carregar exemplo</button>
<button class="btn-danger" id="btn-limpar" disabled>🗑 Limpar</button>
</div>
<div class="stats" id="stats" style="display:none"></div>
<div id="tabela-container">
<p class="vazio">Nenhum dado carregado. Importe um CSV para começar.</p>
</div>
</div>
<script>
// ── Dados de exemplo ──────────────────────────────
const csvExemplo = `nome,email,telefone,cidade,ativo
Ana Paula,ana@email.com,(11) 99999-0001,São Paulo,true
Carlos Silva,carlos@email.com,(41) 99999-0002,Curitiba,false
Beatriz Costa,bia@email.com,(21) 99999-0003,Rio de Janeiro,true
Diego Mendes,diego@email.com,(51) 99999-0004,Porto Alegre,true
Elena Souza,elena@email.com,(31) 99999-0005,Belo Horizonte,false`;
// ── Estado ────────────────────────────────────────
let contatosCarregados = [];
// ── Referências ───────────────────────────────────
const zonaDropEl = document.querySelector("#zona-drop");
const inputArquivo = document.querySelector("#input-arquivo");
const erroEl = document.querySelector("#erro");
const statsEl = document.querySelector("#stats");
const tabelaContainer = document.querySelector("#tabela-container");
const btnExportar = document.querySelector("#btn-exportar");
const btnLimpar = document.querySelector("#btn-limpar");
// ── Parser CSV ────────────────────────────────────
function parsearLinha(linha, delimitador) {
const resultado = [];
let campo = "";
let dentroDeAspas = false;
for (let i = 0; i < linha.length; i++) {
const char = linha[i];
if (char === '"' && !dentroDeAspas) { dentroDeAspas = true; continue; }
if (char === '"' && dentroDeAspas) {
if (linha[i + 1] === '"') { campo += '"'; i++; }
else { dentroDeAspas = false; }
continue;
}
if (char === delimitador && !dentroDeAspas) {
resultado.push(campo.trim());
campo = "";
continue;
}
campo += char;
}
resultado.push(campo.trim());
return resultado;
}
function detectarDelimitador(linha) {
const opts = [",", ";", " ", "|"];
return opts.sort((a, b) =>
(linha.split(b).length - 1) - (linha.split(a).length - 1)
)[0];
}
function parsearCSV(texto) {
const linhas = texto.trim().split(/
?
/).filter(l => l.trim());
if (linhas.length < 2) throw new Error("CSV precisa ter pelo menos cabeçalho e uma linha.");
const delimitador = detectarDelimitador(linhas[0]);
const cabecalho = parsearLinha(linhas[0], delimitador);
return linhas.slice(1).map((linha, i) => {
const valores = parsearLinha(linha, delimitador);
if (valores.length !== cabecalho.length) {
throw new Error(`Linha ${i + 2} tem ${valores.length} campos (esperado: ${cabecalho.length}).`);
}
return cabecalho.reduce((obj, col, j) => {
const val = valores[j];
obj[col] = val === "true" ? true : val === "false" ? false : val;
return obj;
}, {});
});
}
function gerarCSV(dados) {
if (!dados.length) return "";
const cols = Object.keys(dados[0]);
function escapar(v) {
const s = String(v ?? "");
return s.includes(",") || s.includes('"') ? `"${s.replace(/"/g, '""')}"` : s;
}
return [
cols.join(","),
...dados.map(row => cols.map(c => escapar(row[c])).join(","))
].join("
");
}
// ── Renderização ──────────────────────────────────
function renderizarTabela(dados) {
if (!dados.length) {
tabelaContainer.innerHTML = '<p class="vazio">Nenhum registro encontrado.</p>';
statsEl.style.display = "none";
return;
}
const colunas = Object.keys(dados[0]);
const ativos = dados.filter(d => d.ativo === true).length;
statsEl.style.display = "flex";
statsEl.innerHTML = `
<div class="stat"><strong>${dados.length}</strong> Contatos</div>
<div class="stat"><strong>${ativos}</strong> Ativos</div>
<div class="stat"><strong>${dados.length - ativos}</strong> Inativos</div>
<div class="stat"><strong>${colunas.length}</strong> Colunas</div>
`;
const thead = `<thead><tr>${colunas.map(c =>
`<th>${c.charAt(0).toUpperCase() + c.slice(1)}</th>`
).join("")}</tr></thead>`;
const tbody = `<tbody>${dados.map(row =>
`<tr>${colunas.map(col => {
const val = row[col];
if (typeof val === "boolean") {
return `<td><span class="badge ${val ? "badge-sim" : "badge-nao"}">${val ? "Sim" : "Não"}</span></td>`;
}
return `<td>${val ?? ""}</td>`;
}).join("")}</tr>`
).join("")}</tbody>`;
tabelaContainer.innerHTML = `<table>${thead}${tbody}</table>`;
}
function mostrarErro(msg) {
erroEl.textContent = `⚠️ ${msg}`;
erroEl.classList.add("visivel");
setTimeout(() => erroEl.classList.remove("visivel"), 5000);
}
function carregarDados(texto, origem = "arquivo") {
try {
erroEl.classList.remove("visivel");
contatosCarregados = parsearCSV(texto);
renderizarTabela(contatosCarregados);
btnExportar.disabled = false;
btnLimpar.disabled = false;
console.log(`✅ ${contatosCarregados.length} contatos carregados de ${origem}.`);
} catch (erro) {
mostrarErro(erro.message);
}
}
// ── Eventos ───────────────────────────────────────
zonaDropEl.addEventListener("click", () => inputArquivo.click());
inputArquivo.addEventListener("change", (e) => {
const arquivo = e.target.files[0];
if (!arquivo) return;
const reader = new FileReader();
reader.onload = (ev) => carregarDados(ev.target.result, arquivo.name);
reader.readAsText(arquivo, "UTF-8");
});
zonaDropEl.addEventListener("dragover", (e) => {
e.preventDefault();
zonaDropEl.classList.add("arrastando");
});
zonaDropEl.addEventListener("dragleave", () => {
zonaDropEl.classList.remove("arrastando");
});
zonaDropEl.addEventListener("drop", (e) => {
e.preventDefault();
zonaDropEl.classList.remove("arrastando");
const arquivo = e.dataTransfer.files[0];
if (!arquivo) return;
const reader = new FileReader();
reader.onload = (ev) => carregarDados(ev.target.result, arquivo.name);
reader.readAsText(arquivo, "UTF-8");
});
document.querySelector("#btn-exemplo").addEventListener("click", () => {
carregarDados(csvExemplo, "exemplo");
});
btnExportar.addEventListener("click", () => {
if (!contatosCarregados.length) return;
const bom = "uFEFF";
const blob = new Blob([bom + gerarCSV(contatosCarregados)], {
type: "text/csv;charset=utf-8;"
});
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "contatos-exportados.csv";
link.click();
setTimeout(() => URL.revokeObjectURL(url), 100);
});
btnLimpar.addEventListener("click", () => {
contatosCarregados = [];
tabelaContainer.innerHTML = '<p class="vazio">Nenhum dado carregado. Importe um CSV para começar.</p>';
statsEl.style.display = "none";
btnExportar.disabled = true;
btnLimpar.disabled = true;
inputArquivo.value = "";
});
</script>
</body>
</html>
Tarefa para você
Construa um analisador de vendas que:
1. Leia o CSV abaixo (cole como string no código)
data,produto,categoria,quantidade,preco_unitario,vendedor
2025-01-05,Notebook,Eletrônicos,2,3500,Ana
2025-01-07,Mouse,Eletrônicos,5,89.90,Bruno
2025-01-10,Cadeira,Móveis,1,900,Ana
2025-01-12,Teclado,Eletrônicos,3,199.90,Carlos
2025-01-15,Mesa,Móveis,2,700,Bruno
2025-01-18,Monitor,Eletrônicos,4,1200,Ana
2025-01-20,Notebook,Eletrônicos,1,3500,Carlos
2. Calcule e exiba:
a) Total de receita geral
b) Receita por categoria
c) Receita por vendedor (quem vendeu mais?)
d) Produto mais vendido (por quantidade)
e) Ticket médio por venda
3. Exporte um relatório resumido em CSV com:
vendedor, total_vendas, total_receita, ticket_medio
Conclusão
Neste artigo você aprendeu:
- O que é CSV e suas regras de formatação
- Como parsear CSV simples e com campos complexos (aspas, vírgulas internas)
- Conversão automática de tipos após o parse
- Como gerar CSV a partir de objetos JavaScript
- Como exportar CSV para download no navegador
- Como ler arquivos CSV enviados pelo usuário
- Detecção automática de delimitadores
- Quando usar a biblioteca PapaParse
- A comparação prática entre JSON e CSV
📚 Fontes e Referências
- MDN Web Docs — FileReader: https://developer.mozilla.org/pt-BR/docs/Web/API/FileReader
- MDN Web Docs — Blob: https://developer.mozilla.org/pt-BR/docs/Web/API/Blob
- MDN Web Docs — URL.createObjectURL: https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL
- RFC 4180 — Common Format for CSV Files: https://datatracker.ietf.org/doc/html/rfc4180
- PapaParse — Fast CSV Parser for JavaScript: https://www.papaparse.com
- JavaScript.info — File and FileReader: https://javascript.info/file
- Node.js Design Patterns — Mario Casciaro (Packt Publishing)
- CSV Lint — Validador online de CSV: https://csvlint.io