GoSuda

Comparação entre Go 1.25 encoding/json v1 e v2

By lemonmint
views ...

O pacote encoding/json v2 de Go é uma nova implementação que visa melhorar várias deficiências da versão v1 existente (falta de consistência, comportamento inesperado, problemas de desempenho). É um recurso experimental ativado pela tag de compilação goexperiment.jsonv2.

O ponto mais importante é que quando o v2 é ativado, o v1 atua como uma camada de compatibilidade que emula o comportamento do v1 sobre a implementação do v2. Isso é feito através da função DefaultOptionsV1() no arquivo v2_v1.go. Ou seja, o v2 oferece opções para reproduzir completamente o comportamento do v1, ao mesmo tempo em que introduz um novo comportamento padrão mais rigoroso e previsível.

Os principais objetivos do v2 são os seguintes:

  1. Melhorar a precisão e a previsibilidade: Aplica regras mais rigorosas por padrão (por exemplo, diferenciação de maiúsculas e minúsculas, proibição de chaves duplicadas) para reduzir comportamentos inesperados.
  2. Melhorar o desempenho: O motor de parsing e codificação foi redesenhado para aumentar a eficiência.
  3. Aumentar a flexibilidade e o controle: Introduz um sistema de opções detalhado para permitir que os desenvolvedores controlem finamente como o JSON é processado.

Principais diferenças de significado/comportamento

As diferenças de comportamento entre v1 e v2 foram organizadas por item, com foco no arquivo v2_test.go.

1. Correspondência de nomes de campos (diferenciação de maiúsculas e minúsculas)

  • Comportamento do v1: Ao unmarshal membros de objetos JSON para campos de structs Go, a correspondência é não sensível a maiúsculas e minúsculas. Tanto "FirstName" quanto "firstname" são mapeados para o campo FirstName.
  • Comportamento do v2: Por padrão, a correspondência é sensível a maiúsculas e minúsculas, mapeando apenas campos que correspondem exatamente.
  • Razão da mudança: A correspondência não sensível a maiúsculas e minúsculas pode causar comportamentos inesperados e degradação de desempenho ao lidar com campos não correspondentes. O v2 adota um comportamento mais claro e previsível como padrão.
  • Opções relacionadas: No v2, a não diferenciação de maiúsculas e minúsculas pode ser explicitamente ativada por campo usando a opção de tag json:"...,case:ignore", ou globalmente aplicando a opção json.MatchCaseInsensitiveNames(true).

2. Mudança no significado da opção de tag omitempty

  • Comportamento do v1: Omite o campo com base no "estado vazio" do valor Go. "Estado vazio" significa false, 0, ponteiros/interfaces nil, arrays/slices/maps/strings de comprimento zero.
  • Comportamento do v2: Omite o campo com base no "estado vazio" do valor JSON codificado. Ou seja, se for codificado como null, "", {}, [], ele é omitido.
  • Razão da mudança: A definição do v1 é dependente do sistema de tipos de Go. O v2 fornece um comportamento mais consistente com base no sistema de tipos JSON. Por exemplo, no v1, um valor false de tipo bool é omitido, mas no v2, false não é um valor JSON vazio, então não é omitido. O v2 adicionou a opção omitzero para substituir o comportamento do omitempty do v1 que se aplicava a 0 ou false.
  • Opções relacionadas: Se o comportamento do v1 for desejado no v2, use a opção json.OmitEmptyWithLegacyDefinition(true).

3. Mudança no comportamento da opção de tag string

  • Comportamento do v1: Aplica-se a campos de tipo numérico, booleano e string. Recodifica o valor dentro de uma string JSON (por exemplo, int(42) -> "42"). Não se aplica recursivamente a valores dentro de tipos compostos (slices, maps, etc.).
  • Comportamento do v2: Aplica-se apenas a tipos numéricos e recursivamente. Ou seja, números dentro de slices como []int também são codificados como strings JSON.
  • Razão da mudança: O objetivo principal da opção string é representar números como strings para evitar a perda de precisão de inteiros de 64 bits. O comportamento do v1 era limitado e inconsistente. O v2 se concentra nesse uso principal e estende o comportamento recursivamente para torná-lo mais útil.
  • Opções relacionadas: O comportamento do v1 pode ser emulado com a opção json.StringifyWithLegacySemantics(true).

4. Marshalling de slices e maps nil

  • Comportamento do v1: Slices nil e maps nil são marshaled como null.
  • Comportamento do v2: Por padrão, slices nil são marshaled como [] (array vazio) e maps nil como {} (objeto vazio).
  • Razão da mudança: nil é um detalhe de implementação da linguagem Go, e expô-lo no formato JSON, que é independente da linguagem, não é desejável. [] ou {} para representar coleções vazias são expressões mais universais.
  • Opções relacionadas: No v2, é possível marshaling como null como no v1 através das opções json.FormatNilSliceAsNull(true) ou json.FormatNilMapAsNull(true).

5. Unmarshalling de arrays

  • Comportamento do v1: Ao unmarshal para um array Go ([N]T), se o comprimento do array JSON for diferente do comprimento do array Go, nenhum erro é gerado. Se o comprimento for menor, o espaço restante é preenchido com valores zero; se for maior, o excesso é descartado.
  • Comportamento do v2: O comprimento do array JSON deve corresponder exatamente ao comprimento do array Go. Caso contrário, um erro é gerado.
  • Razão da mudança: Arrays de tamanho fixo em Go frequentemente têm um significado importante em termos de seu comprimento. O comportamento do v1 pode levar a perdas silenciosas de dados. O v2 aumenta a precisão com regras mais rigorosas.
  • Opções relacionadas: O comportamento do v1 pode ser emulado com a opção json.UnmarshalArrayFromAnyLength(true).

6. Processamento de time.Duration

  • Comportamento do v1: time.Duration é tratado internamente como int64 e codificado como um número JSON em nanossegundos.
  • Comportamento do v2: É codificado como uma string JSON no formato "1h2m3s" usando o método time.Duration.String().
  • Razão da mudança: Nanossegundos numéricos são menos legíveis, e a representação de string padrão de time.Duration é mais útil.
  • Opções relacionadas: O comportamento do v1 pode ser usado através da opção de tag json:",format:nano" ou da opção json.FormatTimeWithLegacySemantics(true).

7. Tratamento de UTF-8 inválido

  • Comportamento do v1: Durante o marshalling/unmarshalling, se houver bytes UTF-8 inválidos em uma string, eles são silenciosamente substituídos pelo caractere de substituição Unicode (\uFFFD).
  • Comportamento do v2: Por padrão, retorna um erro se encontrar UTF-8 inválido.
  • Razão da mudança: Para evitar a corrupção silenciosa de dados e para aderir ao padrão JSON mais rigoroso (RFC 7493).
  • Opções relacionadas: O comportamento do v1 pode ser emulado com a opção jsontext.AllowInvalidUTF8(true).

8. Tratamento de nomes de membros de objeto duplicados

  • Comportamento do v1: Permite que membros com o mesmo nome apareçam duplicados dentro de um objeto JSON. O valor da última ocorrência sobrescreve o anterior.
  • Comportamento do v2: Por padrão, retorna um erro se houver nomes de membros duplicados.
  • Razão da mudança: O padrão RFC 8259 não define o comportamento de nomes duplicados, o que pode levar a comportamentos diferentes entre implementações. Isso pode ser uma fonte de vulnerabilidades de segurança. O v2 rejeita isso explicitamente para aumentar a precisão e a segurança.
  • Opções relacionadas: O comportamento do v1 pode ser emulado com a opção jsontext.AllowDuplicateNames(true).

Diferenças de implementação e arquitetura

  • v1: Depende muito de decodeState em decode.go e de uma máquina de estado (state machine) escrita manualmente em scanner.go. Esta é uma estrutura monolítica onde a lógica de parsing e a análise semântica estão fortemente acopladas.
  • v2: A arquitetura é mais modular.
    • encoding/json/jsontext: Fornece um tokenizador JSON (Decoder) e um codificador (Encoder) de baixo nível e alto desempenho. Este pacote se concentra apenas nos aspectos sintáticos do JSON.
    • encoding/json/v2: Lida com a conversão semântica entre tipos Go e valores JSON, com base em jsontext.
    • Essa separação melhora a clareza do código e o desempenho, separando a análise sintática da análise semântica.

Novas APIs e recursos do v2

O v2 oferece recursos de controle altamente flexíveis através do sistema json.Options.

  • json.Options: Um conjunto de opções que modificam o comportamento de marshalling/unmarshalling.
  • json.JoinOptions(...): Combina várias opções em uma.
  • WithMarshalers / WithUnmarshalers: Um recurso poderoso que permite injetar lógica de serialização/desserialização para tipos específicos sem precisar implementar as interfaces Marshaler/Unmarshaler. Isso é particularmente útil ao lidar com tipos de pacotes externos.
  • Novas opções: RejectUnknownMembers, Deterministic(false), FormatNilSliceAsNull, etc., que permitem uma variedade de controles de comportamento que não eram possíveis no v1.

Conclusão

O encoding/json v2 é uma implementação moderna que, com base na experiência do v1, melhora significativamente a precisão, o desempenho e a flexibilidade. Embora o comportamento padrão tenha se tornado mais rigoroso, o sistema Options refinado suporta completamente todos os comportamentos do v1, permitindo a introdução gradual dos benefícios do v2, mantendo a compatibilidade com o código existente.

  • Para novos projetos, é recomendável usar o v2 por padrão.
  • Projetos existentes podem continuar usando jsonv1, ou migrar para jsonv2 e adotar gradualmente o comportamento rigoroso do v2 através de DefaultOptionsV1().