JSON, YAML ou TOML: qual usar
Estes três formatos diferem menos no que conseguem expressar do que em como falham, e é nas falhas que você vai gastar o seu tempo.
Cada afirmação desta página foi medida ou tem fonte. Quando não é nem uma coisa nem outra, a página diz isso.
Um pipeline de deploy lê uma lista de países de um arquivo YAML. Alguém acrescenta a Noruega com o código ISO dela, NO, e o pipeline passa a pular esse mercado sem erro nenhum em lugar nenhum. O valor que chegou à aplicação era o booleano false.
Esse é o tipo de coisa que deveria decidir qual formato você escolhe, não uma tabela de “suporta comentários: sim/não”. Os três formatos conseguem guardar um mapa de strings para valores. O que os separa é o que eles fazem com você quando ninguém está olhando.
JSON: chato, e o ponto é exatamente esse
JSON é um formato de fio. Tem seis tipos, quatro caracteres de espaço em branco permitidos (espaço, tabulação, retorno de carro, quebra de linha, segundo a RFC 8259), sem comentários, sem vírgulas finais, sem tipo data, e exatamente um tipo numérico que qualquer leitor está livre para interpretar como float64. Ele é subespecificado em um par de pontos que importam, com destaque para chaves duplicadas, em que a RFC diz que as chaves DEVERIAM ser únicas e depois deixa o comportamento indefinido. JavaScript e Python ficam ambos com a última.
As virtudes dele são inteiramente não técnicas. Toda linguagem traz um parser na biblioteca padrão. Todo cliente HTTP sabe o que fazer com ele. Praticamente não há desencontro de versões: um documento JSON escrito em 2008 faz parsing hoje, de forma idêntica, em toda parte. Quando você serializa para um salto de rede, uma linha de log, uma fila de mensagens ou um cache, nenhum dos recursos voltados a humanos dos outros dois formatos te compra nada, e a universalidade te compra muito.
Os modos de falha são bem batidos e quase todos são sobre números. O Number.MAX_SAFE_INTEGER é 9007199254740991, e IDs acima disso são reescritos em silêncio, o que é um artigo por si só. Datas são strings por convenção e nada faz valer a convenção, o que também é um artigo por si só. Nenhum dos dois é razão para escolher outro formato para transporte. São razões para tomar cuidado.
YAML: ergonomia real, conta real
As pessoas não escolhem YAML porque ele é elegante. Escolhem porque um manifesto do Kubernetes ou um pipeline de CI é uma coisa que um humano edita à mão todo dia, e JSON é genuinamente desagradável de editar à mão: sem comentários, aspas obrigatórias, e uma vírgula faltando quatrocentas linhas acima. YAML te dá comentários, strings multilinha que dá para ler, e nenhum ruído de pontuação. Isso vale alguma coisa.
Eis o que você está pagando.
O problema da Noruega
O YAML 1.1 resolve no, yes, on, off, y e n sem aspas como booleanos. O schema core do YAML 1.2 não faz isso e os deixa como strings. O mesmo documento, a mesma chave, duas respostas:
a: no
Sob a resolução core do YAML 1.2 aquele valor é a string "no". Sob as regras do 1.1 é o booleano false. Qual você recebe depende da sua biblioteca, não do seu arquivo: PyYAML e o Psych do Ruby resolvem pelas regras do 1.1, enquanto o js-yaml segue o 1.2. O yaml.v3 do Go fica no meio: resolve no como string a menos que o campo de destino seja um bool tipado, caso em que ainda aceita a grafia do 1.1. Um serviço em Python e um em Node lendo o mesmo arquivo de configuração discordam sobre o valor, e nenhum dos dois registra nada.
A correção é colocar aspas em toda string que possa ser confundida com outra coisa. Códigos de país, números de versão (1.10 é float, "1.10" não), qualquer coisa começando com zero, e qualquer valor que um usuário forneça. Se você gera YAML programaticamente, faça o emissor colocar aspas defensivamente em vez de confiar na sua própria revisão.
Espaço em branco é sintaxe e tabulação é ilegal
A indentação carrega a estrutura, então uma linha desalinhada é um documento diferente em vez de um erro. Pior, a especificação do YAML proíbe caracteres de tabulação para indentação de forma categórica. Um editor configurado para inserir tabulação produz um arquivo que falha no parsing com uma mensagem sobre um caractere que é invisível no seu terminal. Configure o editor por tipo de arquivo e pare de pensar nisso.
Âncoras se expandem na saída
Âncoras e apelidos deixam você definir um bloco uma vez e reutilizá-lo:
defaults: &defaults
timeout: 30
retries: 3
staging:
<<: *defaults
host: stage.internal
É o recurso que vende YAML para quem mantém quarenta definições de serviço quase idênticas. É também um recurso que o modelo de dados não tem. Converta esse arquivo para JSON e a chave de mesclagem é resolvida, o apelido é expandido, e defaults aparece por inteiro dentro de staging. Volte para YAML e você recebe duas cópias literais. Nada está errado, a rigor, mas aquilo que você mantinha sumiu. Um arquivo YAML que se apoia em âncoras não é realmente conversível, é apenas legível uma vez.
yaml.load executa a sua configuração
O YAML completo suporta tags específicas de linguagem que constroem objetos arbitrários. Em Python isso significa que um documento contendo !!python/object/apply:os.system pode rodar um comando durante o parsing. O yaml.safe_load é a versão que só constrói tipos padrão, e é a que você quer para qualquer coisa que não tenha escrito você mesmo. O PyYAML acabou tornando difícil errar ao exigir um argumento de loader explícito, mas muito código é anterior a isso, e muitas outras linguagens ainda têm um padrão inseguro a uma chamada de distância.
import yaml
with open("config.yaml") as f:
cfg = yaml.safe_load(f) # não yaml.load
O detalhe do superconjunto
O YAML 1.2 foi projetado como superconjunto do JSON, e a especificação afirma que todo documento JSON válido também é um documento YAML 1.2 válido, então um parser 1.2 lê o seu JSON. O YAML 1.1 não bem: ele quer um espaço depois dos dois pontos, então um {"a":1} compacto é erro de parsing lá, e as regras de resolução do 1.1 ainda vão transformar algumas das suas strings em booleanos. Se você depende de “é só entregar o JSON ao parser de YAML”, verifique primeiro qual versão a sua biblioteca implementa. De todo modo, dá para ir no sentido inverso limpo com o conversor de YAML para JSON.
TOML: sem ambiguidade, até aninhar
O TOML existe porque arquivos INI eram agradáveis e imprecisos. Ele conserta a imprecisão: inteiros e floats são tipos distintos, booleanos são só true e false, e existem quatro tipos reais de data e hora (data-hora com deslocamento, data-hora local, data local, hora local) embutidos na gramática em vez de contrabandeados por strings. Comentários são de primeira classe. Definir a mesma chave duas vezes é erro duro em vez de comportamento indefinido, o que é uma coisinha que pega uma classe real de erro de merge.
Para uma configuração plana ou rasa é o melhor dos três. Cargo.toml e pyproject.toml são os casos óbvios: algumas seções, valores string e lista, aninhamento ocasional de um nível. Nada é ambíguo e nada precisa de aspas por segurança.
Fica feio rápido quando o dado é uma árvore. Aninhamento profundo significa ou cabeçalhos longos com pontos ou chaves longas com pontos:
[servers.production.database.replica]
host = "10.0.0.4"
port = 5432
E um array de objetos precisa da forma array de tabelas com colchetes duplos, repetida por elemento:
[[targets]]
name = "web"
port = 8080
[[targets]]
name = "worker"
port = 8081
Isso lê bem com duas entradas. Com trinta entradas de três campos cada, com tabelas em linha que precisam caber numa linha só, você está brigando com o formato. Se a sua configuração é genuinamente hierárquica, o TOML é a forma errada e você vai sentir isso a cada nível que acrescentar.
O que nenhum deles te dá
Um tipo decimal. Os três te dão um float, que é uma aproximação binária. Dinheiro continua pertencendo às unidades menores como inteiro, ou a uma string.
Dados binários. JSON e TOML não têm representação nenhuma, então é base64 numa string. YAML tem uma tag !!binary, que funciona e não sobrevive à conversão para nenhum dos outros dois.
Um schema que venha com o formato. JSON Schema é a opção madura, e como o YAML 1.2 mapeia para o mesmo modelo de dados dá para validar YAML com ele também. É assim que a maior parte da validação de YAML funciona na prática. O TOML não tem equivalente com adoção comparável.
Comentários através de uma conversão. Esta é a porta de mão única. Comentários vivem na sintaxe, não no modelo de dados, então um arquivo YAML ou TOML convertido para JSON perde todo comentário permanentemente, e não existe ferramenta esperta que os traga de volta. Se os comentários de um arquivo sustentam peso, a fonte de verdade é aquele arquivo e o JSON é só um artefato que você gera. A ausência de comentários no JSON é deliberada, e é a razão de essa assimetria existir.
Escolhendo, em forma de perguntas
Uma máquina é a única leitora? JSON. Não faça uma API falar YAML.
Um humano vai editar toda semana, e é hierárquico? YAML, com disciplina de aspas e safe_load.
Um humano vai editar, e são sobretudo seções planas de escalares? TOML. Você não perde nada e ganha tipos sem ambiguidade.
Você precisa que os comentários sobrevivam? Escolha o que escolher, aquele arquivo é a fonte de verdade. Gere para baixo, nunca edite a cópia gerada.
Os valores são produzidos por não desenvolvedores ou por uma interface? JSON, gerado por um programa, validado contra um schema. Toda armadilha de YAML acima é disparada por uma string que alguém digitou.
Você está convertendo entre eles agora? Faça no conversor de JSON para YAML e leia a saída em vez de confiar nela, sobretudo os booleanos, e passe o resultado pelo validador antes que ele chegue a qualquer coisa que faça deploy.