Análise das mudanças de comportamento do encoding/json/v2 no Go 1.25 e da camada de compatibilidade com v1

O pacote encoding/json v2 do Go é uma nova implementação destinada a melhorar várias desvantagens da v1 existente (falta de consistência, comportamentos surpreendentes, problemas de desempenho). Trata-se de um recurso experimental, ativado por meio da tag de build goexperiment.jsonv2.
O ponto mais importante é que , quando a v2 está ativada, a v1 passa a operar como uma camada de compatibilidade que emula o comportamento da v1 sobre a implementação da v2. Isso é feito por meio da função DefaultOptionsV1() no arquivo v2_v1.go. Ou seja, a v2 oferece opções capazes de reproduzir perfeitamente o comportamento da v1 e, ao mesmo tempo, apresenta novos comportamentos padrão mais rigorosos e previsíveis.
Os principais objetivos da v2 são os seguintes.
- Melhoria da correção e da previsibilidade: aplicar, por padrão, regras mais rigorosas (por exemplo, diferenciação de maiúsculas e minúsculas e proibição de chaves duplicadas) para reduzir comportamentos inesperados.
- Melhoria de desempenho: redesenho dos mecanismos de análise e codificação para aumentar a eficiência.
- Maior flexibilidade e controle: introdução de um sistema detalhado de opções que permite ao desenvolvedor controlar minuciosamente o processamento de JSON.
Principais diferenças de semântica e comportamento
Com base principalmente no arquivo v2_test.go, as diferenças de comportamento entre v1 e v2 são organizadas a seguir, item por item.
1. Correspondência de nomes de campos (diferenciação de maiúsculas e minúsculas)
- Comportamento da v1: ao desserializar membros de objetos JSON em campos de structs Go, a correspondência ignora maiúsculas e minúsculas (case-insensitive). Tanto
"FirstName"quanto"firstname"são mapeados para o campoFirstName. - Comportamento da v2: por padrão, a correspondência diferencia maiúsculas de minúsculas (case-sensitive), mapeando apenas campos que coincidem exatamente.
- Motivo da mudança: a correspondência sem distinção de maiúsculas e minúsculas pode causar comportamentos inesperados e degradar o desempenho ao lidar com campos que não correspondem. A v2 adotou, por padrão, um comportamento mais claro e previsível.
- Opções relacionadas: na v2, é possível habilitar explicitamente a desconsideração de maiúsculas e minúsculas por campo, usando a opção de tag
json:"...,case:ignore", ou aplicá-la globalmente comjson.MatchCaseInsensitiveNames(true).
2. Alteração do significado da opção de tag omitempty
- Comportamento da v1: os campos são omitidos com base no "estado vazio" do valor Go. Nesse contexto, "estado vazio" significa
false,0, ponteiros ou interfacesnil, e arrays, slices, mapas ou strings com comprimento zero. - Comportamento da v2: os campos são omitidos com base no "estado vazio" do valor JSON codificado. Ou seja, são omitidos quando codificados como
null,"",{}ou[]. - Motivo da mudança: a definição da v1 depende do sistema de tipos do Go. A v2 toma como base o sistema de tipos do JSON, oferecendo um comportamento mais consistente. Por exemplo, na v1 o valor
falsede um campo do tipoboolé omitido, ao passo que na v2falseé um valor JSON não vazio e, portanto, não é omitido. A v2 acrescenta a opçãoomitzero, que pode substituir o comportamento doomitemptyda v1 aplicado a0oufalse. - Opções relacionadas: para obter na v2 o mesmo comportamento da v1, use a opção
json.OmitEmptyWithLegacyDefinition(true).
3. Alteração do comportamento da opção de tag string
- Comportamento da v1: aplica-se a campos dos tipos numérico, booleano e string. O valor é novamente codificado dentro de uma string JSON (por exemplo,
int(42)->"42"). Não se aplica recursivamente a valores dentro de tipos compostos (slices, mapas etc.). - Comportamento da v2: aplica-se apenas a tipos numéricos e é aplicada recursivamente. Ou seja, os números dentro de um slice como
[]inttambém são todos codificados como strings JSON. - Motivo da mudança: a principal finalidade da opção
stringé representar números como strings para evitar a perda de precisão em inteiros de 64 bits. O comportamento da v1 era limitado e pouco consistente. A v2 concentra-se nesse uso essencial e amplia o comportamento de forma recursiva, tornando-o mais útil. - Opções relacionadas: a opção
json.StringifyWithLegacySemantics(true)permite imitar o comportamento da v1.
4. Desserialização (marshal) de slices e mapas nil
- Comportamento da v1: slices
nile mapasnilsão serializados comonull. - Comportamento da v2: por padrão, slices
nilsão serializados como[](array vazio) e mapasnilcomo{}(objeto vazio). - Motivo da mudança:
nilé um detalhe de implementação da linguagem Go, e não é desejável expô-lo em um formato JSON independente de linguagem.[]e{}são representações mais universais de coleções vazias. - Opções relacionadas: na v2, é possível serializar como
null, como na v1, com as opçõesjson.FormatNilSliceAsNull(true)oujson.FormatNilMapAsNull(true).
5. Desserialização de arrays
- Comportamento da v1: ao desserializar para arrays Go (
[N]T), não é gerado erro quando o comprimento do array JSON difere do comprimento do array Go. Se o array JSON for menor, o espaço restante é preenchido com valores zero; se for maior, o excedente é descartado. - Comportamento da v2: o comprimento do array JSON deve coincidir exatamente com o comprimento do array Go. Caso contrário, ocorre um erro.
- Motivo da mudança: em Go, arrays de tamanho fixo frequentemente têm o comprimento como informação semanticamente importante. O comportamento da v1 pode causar perda silenciosa de dados. A v2 aumenta a correção com regras mais rigorosas.
- Opções relacionadas: a opção
json.UnmarshalArrayFromAnyLength(true)permite imitar o comportamento da v1.
6. Tratamento de time.Duration
- Comportamento da v1:
time.Durationé tratado internamente comoint64e codificado como um número JSON em nanossegundos. - Comportamento da v2: é codificado como uma string JSON no formato
"1h2m3s", usando o métodotime.Duration.String(). - Motivo da mudança: números em nanossegundos são pouco legíveis, e a representação textual padrão de
time.Durationé mais útil. - Opções relacionadas: é possível usar o comportamento da v1 com a opção de tag
json:",format:nano"ou com a opçãojson.FormatTimeWithLegacySemantics(true).
7. Tratamento de UTF-8 inválido
- Comportamento da v1: durante a serialização ou desserialização, bytes UTF-8 inválidos em strings são substituídos silenciosamente pelo caractere de substituição Unicode (
\uFFFD). - Comportamento da v2: por padrão, ao encontrar UTF-8 inválido, retorna um erro.
- Motivo da mudança: evitar a corrupção silenciosa de dados e seguir mais rigorosamente o padrão JSON (RFC 7493).
- Opções relacionadas: a opção
jsontext.AllowInvalidUTF8(true)permite imitar o comportamento da v1.
8. Tratamento de nomes de membros de objeto duplicados
- Comportamento da v1: é permitido que um objeto JSON contenha membros com nomes duplicados. O último valor encontrado sobrescreve os anteriores.
- Comportamento da v2: por padrão, se houver nomes de membros duplicados, retorna um erro.
- Motivo da mudança: a RFC 8259 não define o comportamento para nomes duplicados, o que pode levar a comportamentos diferentes entre implementações. Isso pode ser uma causa de vulnerabilidades de segurança. A v2 rejeita explicitamente essa situação, aumentando a correção e a segurança.
- Opções relacionadas: a opção
jsontext.AllowDuplicateNames(true)permite imitar o comportamento da v1.
Diferenças de implementação e arquitetura
- v1: depende fortemente de
decodeStateemdecode.goe de uma máquina de estados (state machine) escrita manualmente emscanner.go. Trata-se de uma estrutura monolítica, com forte acoplamento entre a lógica de análise sintática e a análise semântica. - v2: a arquitetura é mais modular.
encoding/json/jsontext: fornece um tokenizador (Decoder) e um codificador (Encoder) JSON de baixo nível e alto desempenho. Este pacote se concentra exclusivamente nos aspectos sintáticos do JSON.encoding/json/v2: com base emjsontext, trata a conversão semântica entre tipos Go e valores JSON.- Essa separação faz com que a análise sintática e a análise semântica sejam independentes, melhorando a clareza do código e o desempenho.
Novas APIs e recursos da v2
A v2 oferece controle muito flexível por meio do sistema json.Options.
json.Options: conjunto de opções que alteram o comportamento de serialização e desserialização.json.JoinOptions(...): combina várias opções em uma única.WithMarshalers/WithUnmarshalers: recurso poderoso que permite injetar lógica de serialização e desserialização para tipos específicos sem implementar as interfacesMarshaler/Unmarshaler. É especialmente útil ao lidar com tipos de pacotes externos.- Novas opções:
RejectUnknownMembers,Deterministic(false),FormatNilSliceAsNulle outras, que possibilitam controles de comportamento impossíveis na v1.
Conclusão
O encoding/json v2 é uma implementação moderna que, com base na experiência da v1, melhora significativamente a correção, o desempenho e a flexibilidade. O comportamento padrão tornou-se mais rigoroso, mas, por meio do sofisticado sistema de Options, é possível reproduzir integralmente todos os comportamentos da v1, mantendo a compatibilidade com o código existente e permitindo a adoção gradual das vantagens da v2.
- Para novos projetos, recomenda-se usar a v2 como padrão.
- Projetos existentes podem continuar usando
jsonv1ou migrar parajsonv2, adotando gradualmente o comportamento mais rigoroso da v2 por meio deDefaultOptionsV1().