JSON Toolkit

JSON para CSV: de onde vem o cabeçalho e por que os valores mudam

Resumo: Nove experimentos em um conversor real: a estratégia "chaves da primeira linha" perde campos em silêncio, null, ausência e string vazia viram todos células vazias, um inteiro de 20 dígitos perde precisão já no JSON.parse, e um ponto numa chave faz dois caminhos diferentes colidirem na mesma coluna. Como o cabeçalho é construído, quais valores se distorcem e como escolher uma estratégia conforme o uso a jusante.

Avançado~6 min de leituraPublicado em 2026-11-04Atualizado em 2026-11-04
Estratégias de cabeçalho e fidelidade de valores na conversão de JSON para CSV

Quando uma conversão de JSON para CSV perde dados, quase nunca é durante a etapa de "conversão". A perda acontece em duas perguntas que ninguém fez: quem decidiu o cabeçalho, e cada célula continua reconhecível como o valor que era?

Um campo desaparece em trinta segundos

Muitos conversores — incluindo o pipeline jq idiomático — pegam o cabeçalho apenas do primeiro registro. Reproduza as consequências em Python:

data = [{"id": 1, "name": "A"}, {"id": 2, "email": "b@x.com"}]
first_keys = list(data[0].keys())          # ['id', 'name']
rows = [[r.get(k, "") for k in first_keys] for r in data]
# O e-mail do segundo registro desaparece sem erro nem aviso

A chave email não está no cabeçalho: ela nem sequer tem o direito de virar uma célula vazia — a coluna nunca foi criada, e o valor é descartado em silêncio. A correção é a união de chaves: percorrer todos os registros e criar uma coluna para cada chave que aparecer em qualquer lugar. Os mesmos dados passados pela união produzem o cabeçalho id,name,email, com uma célula vazia onde name falta — e nada se perde.

O conversor direto da nossa página /convert/csv aplica exatamente essa estratégia. Com a entrada [{"id":1,"name":"A"},{"id":2,"email":"b@x.com"}], ele produz:

id,name,email
1,A,
2,,b@x.com

As chaves são ordenadas por primeira aparição e os campos ausentes viram células vazias. jsonoperations e jsonbeam chamam ambas isso de "a correção do bug clássico" — mas "usa a união" é só metade da história. A união tem seu próprio preço; essa é a próxima seção.

Três formas de construir um cabeçalho — e a quem cada uma serve

Estratégia de cabeçalho Como se constrói Força Custo
Chaves da primeira linha Só o primeiro registro é lido Poucas colunas, estável, uma linha de jq Novos campos dos registros seguintes são perdidos em silêncio
União de chaves Todos os registros são lidos Nenhum campo se perde Colunas esparsas e muitos vazios quando as formas variam
Lista explícita de colunas Definida à mão ou por esquema Totalmente controlável, reproduzível Exige manutenção; campos novos pedem atualização manual

O critério de escolha não é "qual é mais sofisticado", mas o que o destino final faz com este CSV:

  • Um colega que dá uma olhada: chaves da primeira linha bastam, as formas de registro costumam ser uniformes.
  • Um relatório ou pipeline de dados: a união de chaves é o piso — uma célula vazia é, ela própria, informação.
  • Um export automático recorrente: a lista explícita de colunas. Deriva de forma (campo adicionado a montante, chave renomeada) deve falhar com barulho, não produzir em silêncio uma tabela que ninguém percebe estar errada.

A terceira opção é a mais negligenciada. Em automação, "a conversão teve sucesso" e "a conversão está correta" são duas afirmações diferentes — e a lista explícita de colunas é o único mecanismo que as separa.

Quando uma chave contém um ponto, os caminhos de achatamento colidem

O achatamento em notação de ponto transforma {"a": {"b": 2}} na coluna a.b. Mas e se a chave em si contiver um ponto? Testado com pandas:

import pandas as pd
d = [{"a.b": 1, "a": {"b": 2}}]
pd.json_normalize(d, sep=".")
# Apenas uma coluna sobrevive: ['a.b'] — duas chaves totalmente diferentes colidem no mesmo caminho

A chave literal a.b e o caminho aninhado a → b produzem o mesmo nome de coluna, um sobrescreve o outro, e os dados são corrompidos em silêncio. É um exemplo construído, mas acontece com dados reais: nomes de métricas de monitoramento (cpu.usage) e campos de log (error.type) adoram pontos.

Dois remédios:

  1. Trocar o separador. sep="_" ou um separador personalizado, depois de verificar que as chaves originais nunca o contêm.
  2. Trocar a notação de caminhos. JSON Pointer usa / como separador (/a/b); o conversor Excel do toolvore segue esse caminho. Ele tem a mesma classe de ambiguidade — uma chave com / deve ser escrita como ~1 — mas as regras de escape são normalizadas na RFC 6901 em vez de inventadas por cada ferramenta.

Qualquer que seja a escolha, escreva a regra na documentação do export. O cabeçalho CSV é o único "esquema" que esses dados carregam; o leitor não encontra suas regras de achatamento em lugar nenhum.

Arrays: três estratégias, cada uma perdendo algo diferente

Arrays são o verdadeiro obstáculo do JSON para CSV: uma célula não pode conter vários valores. Três estratégias, com perdas verificadas na prática:

Estratégia Forma de saída O que se perde Ideal para
JSON-na-célula O array serializado como string JSON na célula Ferramentas de tabela não conseguem filtrar nem agregar os elementos Fidelidade primeiro, inspeção manual
Colunas indexadas tags.0, tags.1, uma coluna cada O número de colunas explode com o array mais longo; muitos vazios Arrays pequenos de comprimento fixo
Linhas explodidas Uma linha por elemento, campos pai duplicados Inflação de linhas; comportamento de arrays vazios precisa ser definido à parte Análise de detalhe, agregação por elemento

Nossos conversores /convert/csv e /convert/table usam JSON-na-célula. A entrada [{"orderId":"A001","items":[{"sku":"X1","qty":2},{"sku":"Y9","qty":1}]}] produz:

orderId,items
A001,"[{""sku"":""X1"",""qty"":2},{""sku"":""Y9"",""qty"":1}]"

Quando você precisa de linhas explodidas, o record_path do pandas faz isso direto:

orders = [{"orderId": "A001", "total": 99,
           "items": [{"sku": "X1", "qty": 2}, {"sku": "Y9", "qty": 1}]}]
items = pd.json_normalize(orders, record_path=["items"],
                          meta=["orderId", "total"], sep=".")
# Duas linhas de detalhe; orderId e total duplicados em cada uma

Ao explodir há três armadilhas para decidir antes: explodir vários arrays de uma vez constrói um produto cartesiano; arrays vazios não geram linha nenhuma por padrão (sua lista de clientes perde pessoas); e campos pai duplicados exigem deduplicação quando você agrega por orderId a jusante. O guia do convertcsvonline lista "decidir primeiro a granularidade" como primeiro passo exatamente por isso — fixada a granularidade, as três perguntas têm cada uma uma única resposta.

Testado: valores que saem mudados da conversão

Um teste de fidelidade executado no nosso conversor real (Node v22.13.1 + papaparse, a mesma implementação do /convert/csv):

1. null, ausência e string vazia viram todos células vazias. Entrada [{"a":null},{"b":""},{"c":1}] — as três células saem vazias. O CSV não tem três estados, só "há caracteres" e "não há caracteres". Depois de um round-trip, os três se leem como ""; a semântica original é irrecuperável.

2. Objetos e arrays aninhados viram strings JSON. As aspas são duplicadas conforme a RFC 4180 ({""name"":""张三""}), e ao colar a célula em qualquer parser JSON o valor volta — por isso o JSON-na-célula preserva a fidelidade.

3. Um inteiro de 20 dígitos perde precisão já no parse — e a culpa não é do CSV. Entrada "id": 9007199254740993; no CSV já está 9007199254740992. O JSON.parse do JavaScript usa doubles IEEE 754: a perda acontece no parsing, antes de qualquer conversor CSV. O json.load do Python preserva integralmente. Campos tipo identificador (IDs snowflake, números de pedido) deveriam chegar como strings desde o produtor. Mais sobre precisão numérica em por que números grandes perdem precisão.

4. Zeros à esquerda em strings estão seguros na camada de texto do CSV. "phone": "00123" continua 00123 no CSV, porque é string do começo ao fim e nada o arranha. Quem arranha os zeros é a etapa posterior em que o Excel trata a célula como número — veja quatro formas de converter JSON em Excel.

5. Campos com vírgulas, aspas ou quebras de linha. O papaparse segue a RFC 4180: citar o campo, duplicar as aspas internas. "He said \"hi\", ok" sai como "He said ""hi"", ok" e sobrevive intacto ao round-trip. Serializadores escritos à mão que contornam uma biblioteca quase sempre falham exatamente aí.

Round-trip: o que o interruptor de inferência muda

CSV → JSON precisa adivinhar um tipo para cada célula. Nosso conversor desliga a inferência de tipos por padrão e devolve tudo como string:

[{ "id": "9007199254740992", "phone": "00123" }]

Com typeInference ativado, id vira número (a precisão já se foi), enquanto phone continua string — as regras de inferência excluem formas com zero à esquerda como 010, que não são literais numéricos legais, então a string sobrevive por sorte.

Grave essa assimetria: inferência desligada no pior caso custa tipos; inferência ligada pode custar dados. Colunas que precisam de números devem ser convertidas por definições explícitas de coluna, não confiadas a uma inferência global. A implementação rowsToRecords do nosso artigo anterior JSON, CSV e dados tabulares é exatamente esse princípio em código.

Escolher a estratégia conforme o destino: uma tabela de decisão

Quem recebe o CSV Estratégia de cabeçalho Tratamento de arrays Tipos
Um colega que abre uma vez União de chaves JSON-na-célula Sem inferência
Excel / relatórios União de chaves JSON-na-célula, ou colunas indexadas se comprimento fixo Colunas de ID forçadas como texto
Importação em banco de dados Lista explícita de colunas Tabelas mestre/detalhe separadas, ou explosão Definições explícitas de coluna
BI / agregação Lista explícita de colunas Linhas explodidas, granularidade = menor unidade de análise Definições explícitas de coluna
Pipeline automatizado Lista explícita de colunas JSON-na-célula, explosão a jusante Inferência desligada, conversão coluna a coluna

A lei comum: quanto mais perto do humano, mais se pode permitir afrouxar; quanto mais perto da máquina, mais tudo precisa ser explícito.

Uma checklist para copiar

  • [ ] O cabeçalho vem de uma união completa de chaves ou de uma lista explícita? Se não, você consegue provar que nenhum registro perdeu campo?
  • [ ] As chaves podem conter pontos ou barras? A colisão com o separador de achatamento foi verificada?
  • [ ] A estratégia de arrays está documentada? O comportamento de arrays vazios está decidido?
  • [ ] Para null, ausência e string vazia: o que o destino lê de cada um? Escolha uma regra e congele-a.
  • [ ] Colunas tipo identificador (IDs, números de pedido, telefones) viajam como strings de ponta a ponta?
  • [ ] Depois da explosão, o número de linhas é igual à soma dos comprimentos dos arrays? Verificado por amostragem?
  • [ ] Campos de exemplo com vírgulas, aspas e quebras de linha sobrevivem intactos a um round-trip?
  • [ ] A inferência global de tipos está desligada no reimport, com definições explícitas para colunas numéricas?

Só quando as oito têm resposta clara é que este CSV está "correto" — e não apenas "convertido com sucesso".

FAQ

Por que o conversor não achata objetos aninhados em colunas por padrão?

O achatamento faz o número de colunas fugir ao controle: aninhamento profundo produz nomes como user.address.geo.lat, e combinado com a união de chaves, um único registro de outra forma adiciona uma pilha de colunas. Strings JSON nas células preservam a fidelidade e um número estável de colunas; achate explicitamente quando precisar.

E se o cabeçalho contiver nomes de coluna duplicados?

A colisão de duas chaves diferentes após o achatamento (como a.b contra o aninhado a→b) deve ser resolvida a montante: troque o separador ou passe para JSON Pointer. Para duplicatas dentro do mesmo objeto, os conversores em geral guardam o último valor; duplicatas no cabeçalho dependem da implementação — o unparse do papaparse não deduplica, e o Excel funde ou descarta duplicatas em silêncio ao abrir. É por isso que está na checklist.

Campos pai duplicados após a explosão são um bug?

Não — é design. Uma linha explodida é uma linha de detalhe, e uma linha de detalhe deve poder ser religada ao registro pai por chave. Agregue a jusante pela chave pai; se o tamanho preocupa, exporte o arquivo de detalhe e uma tabela mestre separada.

Como verificar que minha conversão não perdeu nenhum campo?

Faça uma auditoria de chaves: calcule a união de todas as chaves do JSON de origem, depois o conjunto de cabeçalhos de coluna do CSV, e faça a diferença. Uma diferença não vazia significa campos perdidos. Em pipelines em lote, codifique isso como uma asserção nos seus testes: qualquer troca de conversor que comece a perder campos falha automaticamente.