O que se perde ao converter entre JSON, YAML, CSV e XML
Qualquer conversão entre formatos de dados deita fora aquilo que a origem conseguia exprimir e o destino não. Algumas perdas são óbvias: os comentários desaparecem no momento em que o YAML se torna JSON. A maioria é silenciosa. Uma data chega como cadeia de carateres, um array transforma-se em cinco colunas, um ID que acabava em 3 passa a acabar em 2. O hábito útil é saber que detalhe se vai perder antes de ele se perder.
O que cada formato consegue transportar
| Comentários | Tipos | Aninhamento | Datas | Ordem das chaves | Chaves duplicadas | |
|---|---|---|---|---|---|---|
| JSON | Não | string, number, boolean, null, object, array | Sim | Sem tipo nativo | Não ordenada pela especificação, preservada pela maioria dos analisadores | Indefinido, ganha normalmente a última |
| YAML | Sim, # | Os do JSON, mais timestamps e outros no 1.1 | Sim | Sim no 1.1, dependente de etiqueta no 1.2 | Preservada no ficheiro, não depois da leitura | Inválidas, mas muitas vezes aceites |
| CSV | Não | Nenhuns, cada célula é texto | Não, só linhas planas | Não | Fixada pela linha de cabeçalho | Válidas e ambíguas |
| XML | Sim, <!-- --> | Nenhuns sem um esquema, tipados com XSD | Sim | Só através de um esquema | Elementos ordenados, atributos não | Elementos repetidos são normais, atributos duplicados são um erro |
| TOML | Sim, # | string, integer, float, boolean, date, time, date-time, array, table | Sim, através de tabelas | Sim, de primeira classe | Não é significativa | Um erro, sempre |
Duas colunas causam a maior parte das surpresas. Os tipos decidem se 007 sobrevive ou se chega como o número 7, e o aninhamento decide se é possível converter para CSV sem inventar uma convenção.
O CSV é plano, e cada célula é texto
O CSV tem uma única forma: linhas de campos. Não há maneira de exprimir um objeto numa célula, por isso um conversor tem de inventar alguma coisa. Normalmente achata a estrutura com caminhos separados por pontos, e assim isto:
[{ "id": 1, "name": { "first": "Ada" }, "tags": ["admin", "ops"] }]
passa a ser:
id,name.first,tags.0,tags.1
1,Ada,admin,ops
Isso desfaz-se sem problemas até acontecerem duas coisas. Arrays de comprimentos diferentes alargam o cabeçalho até ao registo mais comprido, por isso uma linha com três etiquetas acrescenta uma coluna tags.2 e deixa todas as outras linhas com uma célula vazia. E uma chave que já contenha um ponto passa a ser indistinguível de um caminho aninhado, pelo que o caminho de volta reconstrói a estrutura errada. A alternativa é uma cadeia JSON dentro da célula, "[""admin"",""ops""]" com as aspas duplicadas como o CSV exige, que não perde nada e que nada a jusante compreende.
Os tipos são a segunda metade. Uma célula contém carateres, por isso quem lê tem de adivinhar. Adivinhar dá 007 como 7, 1.0 e 1 como um só valor, um código postal sem o zero à esquerda, um código de empresa TRUE como booleano. Não adivinhar dá todos os números como cadeias de carateres. A inferência de tipos no CSV to JSON, ou no TSV to JSON, é um palpite com valores por omissão sensatos e não uma recuperação, porque os tipos originais nunca chegaram a ser escritos.
Uma célula em branco tanto pode significar cadeia vazia, null, zero ou não aplicável; decida o que significa e diga-o, porque o ficheiro não o consegue dizer. Trocar de separador, como faz o CSV to TSV, é a única conversão segura aqui: não há tipos que se possam estragar.
O YAML adivinha o tipo por si
O YAML infere os tipos a partir de texto sem aspas, que é o que o torna agradável de escrever e arriscado de converter.
country: no
version: 1.20
build: 010
start: 12:30
Com um analisador de YAML 1.1, que é o que o PyYAML e muitas bibliotecas mais antigas ainda implementam, isso é:
{ "country": false, "version": 1.2, "build": 8, "start": 750 }
Os quatro mudaram de significado. no é o problema da Noruega: o YAML 1.1 trata y, yes, no, on e off como booleanos, por isso o código do país passa a false. 1.20 é um float, logo o zero final desaparece. 010 corresponde ao padrão octal e passa a 8. 12:30 corresponde ao padrão sexagesimal e passa a 750, uma contagem de minutos.
Um analisador do esquema base de YAML 1.2, como o js-yaml atual, lê o mesmo ficheiro com "no" como cadeia, 010 como 10 e "12:30" como cadeia. Um ficheiro, dois analisadores, dados diferentes. As especificações estão genuinamente em desacordo; a 1.2 abandonou de propósito as regras sexagesimal e das palavras booleanas.
A defesa é um único caráter. Ponha entre aspas tudo o que deva ser texto: country: "no", version: "1.20". O conversor de YAML e JSON mostra o que um analisador fez de um ficheiro, porque o JSON não tem onde esconder a ambiguidade.
O JSON não tem datas, não tem comentários e tem um teto de 53 bits
Os números em JSON são doubles IEEE 754, por isso os inteiros só se mantêm exatos até 2^53, ou seja 9007199254740992. A partir daí:
{ "id": 9007199254740993 }
Leia isso e volte a serializá-lo na maioria das linguagens e o que volta é 9007199254740992. O valor não chega sequer a ser representável, por isso não está tanto arredondado como indisponível. É por isto que o Discord, o Twitter e outras APIs com identificadores snowflake enviam os ID como cadeias de carateres: um inteiro de 64 bits não cabe num número JSON.
Também não há tipo de data, por isso as datas viajam como cadeias, por convenção em ISO 8601, e nada marca "2026-08-25" como data em vez de texto. Ao passar por TOML ou por um analisador de YAML 1.1 pode tornar-se uma data verdadeira e voltar formatada de outra maneira. O JSON também não tem comentários nem vírgulas finais, por isso as anotações de uma origem YAML ou TOML desapareceram, não mudaram de sítio. Gerar interfaces de TypeScript a partir de uma amostra herda os dois pontos cegos: uma cadeia não é uma data, e um campo ausente não é um campo opcional.
O XML não cabe dentro do JSON, em nenhum dos sentidos
O XML distingue atributos de elementos filhos, e o JSON só tem chaves.
<user id="7" active="true">
<name>Ada</name>
<tag>admin</tag>
<tag>ops</tag>
</user>
Um conversor escolhe uma convenção, normalmente prefixando os atributos com @ e juntando os elementos repetidos num array. Isso perde informação de uma forma específica: com um único <tag> não há array, por isso a forma da saída depende dos dados e não do esquema, e código que espera uma lista parte-se no registo que só tem uma etiqueta. Conteúdo misto como <p>Hello <b>there</b> friend</p> não tem equivalente em JSON, e os espaços de nomes, o CDATA e os comentários desaparecem.
O sentido inverso perde outras coisas. Os nomes de elementos não podem começar por um dígito nem conter espaços, por isso chaves JSON arbitrárias têm de ser deformadas. null passa a um elemento vazio ou a xsi:nil, por convenção. Um array de topo precisa de um elemento envolvente que nunca esteve na origem. E nada no JSON regista se um valor já foi um atributo, pelo que o JSON to XML só emite elementos. O CSV to XML faz essa escolha às claras, perguntando se quer elementos ou atributos.
Ordem das chaves e chaves duplicadas
A ordem das chaves não é garantida em lado nenhum e é preservada em quase todo o lado. A especificação do JSON diz que os objetos não são ordenados, e no entanto todos os analisadores de referência mantêm a ordem de inserção. Ordenar as chaves continua a ser mais seguro para tudo o que seja comparado ou sujeito a soma de verificação, já que uma diferença entre dois documentos cujas chaves apenas mudaram de sítio é ilegível. Comparar estruturalmente, como faz a ferramenta de comparação de JSON, faz corresponder por chave e não por posição, e evita a questão.
As chaves duplicadas são a aresta mais afiada. O JSON não diz nada, por isso os analisadores divergem e a maioria guarda em silêncio o último valor. O YAML proíbe-as e muitos analisadores aceitam-nas na mesma. O TOML rejeita-as sem apelo. O CSV permite duas colunas com o mesmo cabeçalho e deixa a decisão a quem lê, razão suficiente para passar primeiro um validador de CSV por um ficheiro desconhecido. O XML é o único em que a repetição é significativa e não acidental.
Converter de propósito
Qualquer cadeia de processamento tem um passo com perdas. Escolha qual é, em vez de o descobrir mais tarde.
- Deixe o formato mais estreito para o fim. De XML para JSON e depois para CSV perdem-se os atributos no primeiro passo e o aninhamento no segundo. Se o destino for CSV, decida que campos interessam e achate a estrutura deliberadamente.
- Ponha entre aspas antes de converter, não depois. Números de versão, códigos de país, códigos postais e tudo o que tenha um zero à esquerda devem estar entre aspas na origem. Depois de
noserfalse, o texto original é irrecuperável. - Verifique a saída enquanto dados. Formate-a, confirme a contagem de registos e inspecione uma linha que já sabe ser problemática: a vírgula num nome, o array mais comprido, o maior ID.
- Compare uma ida e volta. Converta para fora, converta de volta, compare. O que a comparação assinalar é aquilo que a conversão não consegue transportar.
- Guarde o original. O ficheiro convertido é um derivado e, quando alguém perguntar daqui a três meses se um campo estava vazio ou ausente, só a origem consegue responder.