JSON Toolkit

Convertendo JSON de e para CSV e dados tabulares

Resumo: CSV é bidimensional, JSON tem forma de árvore. A parte difícil da conversão não é o código — é decidir de antemão como os arrays serão achatados e como os tipos serão preservados.

Iniciante~10 min de leituraPublicado em 2026-10-30Atualizado em 2026-10-30
Capa de conversão entre JSON ↔ CSV e dados tabulares

Resumo da ópera: achate com chaves pontuadas e arrays serializados em JSON quando pessoas vão ler a exportação, divida em uma tabela pai mais tabelas relacionadas quando for carregar um banco de dados, e expanda arrays em linhas extras apenas para pivôs; ao restaurar, converta por definições explícitas de coluna em vez de inferência global.

Achatar uma árvore em uma tabela sempre perde hierarquia; restaurar uma tabela em uma árvore sempre adivinha tipos. As duas direções precisam ter suas regras declaradas de antemão.

1. O que o CSV realmente especifica

A RFC 4180 tem só algumas páginas e especifica menos do que muita gente imagina:

  1. Cada registro ocupa uma linha, com campos separados por vírgulas;
  2. O terminador de linha é CRLF, embora na prática \n também seja aceito;
  3. Um campo que contém vírgula, aspa dupla ou quebra de linha precisa estar entre aspas duplas;
  4. Aspas duplas dentro de um campo são escritas como duas aspas duplas "";
  5. A primeira linha pode ser um cabeçalho;
  6. Nenhuma codificação de caracteres é especificada, nem tipos.
id,name,remark
1,张三,"喜欢 JSON,讨厌尾逗号"
2,李四,"他说:""这行有引号"""
3,王五,"多行
备注"

Preste atenção à regra 3: campos contendo quebras de linha são legais, então um analisador ingênuo que divide por \n e depois por , com certeza está errado.

2. JSON → CSV: decida a estrutura primeiro

Há apenas três estratégias para achatar uma estrutura de árvore em uma tabela bidimensional:

Estratégia 1: achatar campos (a mais comum)

{ "id": 1, "user": { "name": "张三", "city": "杭州" }, "tags": ["a", "b"] }
id,user.name,user.city,tags
1,张三,杭州,"[""a"",""b""]"
  • Una as chaves de objetos aninhados com .;
  • Arrays não podem ser expressos como colunas, então faça JSON.stringify deles direto na célula (o que ainda é um campo CSV legal);
  • Desvantagem: ferramentas de planilha não conseguem filtrar o array, então é preciso analisá-lo de novo.

Estratégia 2: expandir o array em várias linhas

id,user.name,tag
1,张三,a
1,张三,b

Isso convém a relações um-para-muitos em que os consumidores a jusante agregam por elemento. A desvantagem é duplicar os campos do pai e multiplicar o número de linhas.

Estratégia 3: dividir em várias tabelas

Uma tabela principal mais tabelas relacionadas unidas por um id — a abordagem mais limpa, mas vai além de "uma única exportação" e se parece mais com modelagem de dados.

Qual usar? Use a estratégia 1 para exportações destinadas a humanos, a estratégia 3 para carregar em um banco de dados e a estratégia 2 para pivotar dados.

3. CSV → JSON: como os tipos são adivinhados

O CSV não tem tipos, então cada etapa da restauração é um palpite:

Conteúdo da célula Significado possível Risco de converter direto para número
00123 Número de funcionário, CEP Zeros à esquerda perdidos → 123
1.10 Número de versão, valor Vira 1.1
1e5 Identificador ou notação científica Vira 100000
2026-10-30 Data ou string Ambiguidade em fuso horário e formato
13800138000 Número de telefone Ainda seguro com 11 dígitos, mas alguns IDs perdem precisão
"" String vazia ou null Ambiguidade semântica

Padrão sugerido: trate tudo como string e deixe um schema ou uma definição explícita de coluna decidir quais colunas viram números ou booleanos. "Inferência inteligente" é encantadora ao exportar e uma fonte de incidentes ao importar.

type ColumnType = 'string' | 'number' | 'boolean' | 'json';

/**
 * Converte linhas CSV em objetos conforme as definições de coluna.
 * @param rows O array de linhas cru; a primeira linha é o cabeçalho
 * @param columnTypes Mapeamento de nome de coluna para tipo
 * @returns O array de objetos convertido
 */
function rowsToRecords(
  rows: string[][],
  columnTypes: Record<string, ColumnType>,
): Array<Record<string, unknown>> {
  const [headerRow, ...dataRows] = rows;

  return dataRows.map((dataRow) => {
    const record: Record<string, unknown> = {};

    headerRow.forEach((columnName, columnIndex) => {
      const rawValue = dataRow[columnIndex] ?? '';
      const columnType = columnTypes[columnName] ?? 'string';

      if (columnType === 'number') {
        const numericValue = Number(rawValue);
        record[columnName] = Number.isFinite(numericValue) ? numericValue : null;
        return;
      }
      if (columnType === 'boolean') {
        record[columnName] = rawValue === 'true' || rawValue === '1';
        return;
      }
      if (columnType === 'json') {
        try {
          record[columnName] = JSON.parse(rawValue);
        } catch {
          record[columnName] = rawValue;
        }
        return;
      }
      record[columnName] = rawValue;
    });

    return record;
  });
}

4. Três armadilhas em torno do Excel

4.1 Codificação

Quando o Excel abre um CSV UTF-8 sem BOM, o texto em chinês vira mojibake. Adicionar um BOM (\uFEFF) na exportação é a correção de compatibilidade mais barata:

const bom = '\uFEFF';
const csvContent = `${bom}${headerLine}\n${bodyLines.join('\n')}`;

4.2 Números longos virando notação científica

Um documento de identidade de 18 dígitos ou um número de pedido de 19 dígitos aparece no Excel como 1.23457E+17, e o valor em si já foi truncado. Prefixar o campo com uma tabulação ou envolvê-lo como fórmula (="1234...") preserva a formatação de texto, mas transforma a célula em uma fórmula. A abordagem mais segura é orientar os usuários a "usar o recurso de importação e especificar a coluna como texto", ou oferecer um .xlsx junto com a exportação.

4.3 Delimitadores afetados pela localidade

Algumas localidades europeias usam o ponto e vírgula ; como separador por padrão. Ao distribuir entre regiões, ou fixe o CSV em vírgulas e diga isso, ou ofereça uma versão TSV (separada por tabulações) — o TSV tolera melhor as localidades.

5. Lidando com arquivos grandes

Não carregue CSVs com mais de cem mil linhas na memória de uma vez:

import { createReadStream } from 'node:fs';
import { parse } from 'csv-parse';

const parser = createReadStream('orders.csv').pipe(
  parse({ columns: true, skipEmptyLines: true, bom: true }),
);

for await (const record of parser) {
  // Processa um registro por vez; o uso de memória independe do tamanho do arquivo
}

A direção de exportação funciona do mesmo jeito: gere e escreva em um stream conforme avança, em vez de concatenar uma única string gigante.

6. Autoverificação de consistência de ida e volta

Uma ida e volta CSV (JSON → CSV → JSON) quase nunca é exatamente idêntica, mas você pode definir quais perdas são aceitáveis:

Verificação Expectativa
Contagem de campos Idêntica
Valores de string Idênticos (incluindo zeros à esquerda)
Tipos numéricos Determinados pela definição de coluna, não por inferência
Objetos aninhados Recuperáveis por caminhos pontuados
Arrays Visíveis após serialização em string JSON
Valores vazios Normalizados para null ou para a string vazia — escolha exatamente uma opção

Colocar esta tabela em testes é bem mais eficaz do que um acordo verbal.

7. Lista de verificação

  • [ ] A semântica de "uma linha é um registro" está declarada explicitamente e refletida no nome do arquivo de exportação;
  • [ ] Toda coluna que pode perder precisão é exportada como string;
  • [ ] CSVs em chinês incluem BOM, ou a codificação está declarada explicitamente;
  • [ ] Campos com vírgulas, aspas ou quebras de linha são escapados corretamente (use uma biblioteca, não concatene strings sozinho);
  • [ ] Arquivos grandes passam por leitura e gravação em streaming;
  • [ ] As importações convertem tipos pela definição de coluna, nunca por inferência inteligente global.

Perguntas frequentes

Como escolher entre achatar, expandir linhas e dividir tabelas?

Achate para exportações destinadas a humanos: una chaves aninhadas com . e faça JSON.stringify dos arrays na célula, ao custo de não conseguir filtrar esses arrays em uma planilha. Divida em uma tabela pai mais tabelas relacionadas quando o destino for um banco de dados. Expanda arrays em linhas extras quando os consumidores a jusante agregarem por elemento, aceitando campos do pai duplicados.

Por que uma importação CSV não deve inferir tipos automaticamente?

A RFC 4180 especifica delimitadores e escaping, mas não tipos, então cada etapa de restauração é um palpite: 00123 perde os zeros à esquerda, 1.10 vira 1.1 e 1e5 vira 100000. Trate tudo como string por padrão e deixe um schema ou uma definição explícita de coluna decidir quais colunas viram números ou booleanos.

Devo esperar que uma ida e volta CSV reproduza exatamente o JSON original?

Quase nunca. Defina antes as perdas aceitáveis: mesma contagem de campos, mesmos valores de string incluindo zeros à esquerda, tipos numéricos decididos pela definição de coluna, objetos aninhados recuperáveis por caminhos pontuados, arrays visíveis após serialização e valores vazios normalizados para null ou para a string vazia — escolha exatamente uma opção.