O que se perde ao converter entre JSON, YAML, CSV e XML

Como o JSON, o YAML, o CSV, o XML e o TOML diferem quanto a comentários, tipos, aninhamento, datas e ordem das chaves, e quais desses detalhes cada conversão entre eles deita fora sem avisar.

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áriosTiposAninhamentoDatasOrdem das chavesChaves duplicadas
JSONNãostring, number, boolean, null, object, arraySimSem tipo nativoNão ordenada pela especificação, preservada pela maioria dos analisadoresIndefinido, ganha normalmente a última
YAMLSim, #Os do JSON, mais timestamps e outros no 1.1SimSim no 1.1, dependente de etiqueta no 1.2Preservada no ficheiro, não depois da leituraInválidas, mas muitas vezes aceites
CSVNãoNenhuns, cada célula é textoNão, só linhas planasNãoFixada pela linha de cabeçalhoVálidas e ambíguas
XMLSim, <!-- -->Nenhuns sem um esquema, tipados com XSDSimSó através de um esquemaElementos ordenados, atributos nãoElementos repetidos são normais, atributos duplicados são um erro
TOMLSim, #string, integer, float, boolean, date, time, date-time, array, tableSim, através de tabelasSim, de primeira classeNão é significativaUm 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 no ser false, 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.