Fale conosco
Desenvolvimento

Go 1.27: encoding/json/v2 muda os padrões e o mesmo struct passa a gerar outro JSON

38 min de leitura
Go 1.27: encoding/json/v2 muda os padrões e o mesmo struct passa a gerar outro JSON

Go 1.27: encoding/json/v2 muda os padrões e o mesmo struct passa a gerar outro JSON

O encoding/json do Go carrega decisões de 2011 que nunca puderam ser corrigidas sem quebrar a compatibilidade prometida pela linguagem: nomes de campos duplicados aceitos com "vence o último", JSON com UTF-8 inválido passando silenciosamente, campo com maiúscula diferente do struct sendo preenchido por engano. A resposta do time do Go foi criar um namespace novo — encoding/json/v2 e encoding/json/jsontext — estáveis desde o Go 1.27 (agosto de 2026) e explicados em detalhe no post oficial "A new Go API for JSON", publicado em 4 de novembro de 2026 junto de um guia de migração.

O detalhe que importa: o encoding/json que você já usa agora roda sobre o motor novo . Desde o Go 1.27, o pacote v1 é implementado em cima da v2 com um conjunto de opções de compatibilidade ligadas. O comportamento de saída continua o mesmo, mas as mensagens de erro podem mudar — e, no momento em que você troca o import, várias decisões de serialização mudam sem que o compilador reclame.

Para quem expõe API HTTP, consome webhook de terceiro ou grava payload em fila, isso não é detalhe de biblioteca: é mudança de contrato.

Dois pacotes, uma divisão de responsabilidades

A arquitetura separa sintaxe de semântica:

encoding/json/jsontext lida com a gramática JSON: tokens, Value , formatação, indentação, streaming. Trabalha sem reflexão.

encoding/json/v2 lida com a tradução entre valores Go e JSON: structs, tags, custom marshalers.

Essa separação é o que permite validar ou redigir um payload grande sem materializar struct nenhuma. E é de onde vem o ganho de desempenho: o Unmarshal da v2 chega a ser cerca de 10x mais rápido que o v1 em benchmarks divulgados pela equipe. Já o Marshal não acompanha o mesmo ganho — em medições independentes ele empata ou fica levemente mais lento em alguns payloads. Ou seja: não troque de pacote esperando que o gargalo de serialização de resposta desapareça.

As cinco diferenças que compilam e quebram cliente

Nenhuma delas gera erro de compilação. Todas geram diferença de saída ou de aceitação.

1. Campos desconhecidos viram erro

Na v1, um JSON com membro que o struct não conhece é ignorado. Na v2, é erro por padrão — a postura passa de tolerante para estrita.

Isso é ótimo para endpoint interno que valida entrada e péssimo para consumir webhook de parceiro que adiciona campos sem avisar. A saída é jsonv2.DiscardUnknownMembers(true) .

2. Nomes duplicados viram erro

{"total":100,"total":0} era aceito com o último valor vencendo. Agora é rejeitado ( jsontext.AllowDuplicateNames(true) restaura). Do lado de segurança isso fecha uma classe de divergência entre o que seu serviço entende e o que o serviço vizinho entende sobre o mesmo payload.

3. Match de campo passa a ser case-sensitive

A v1 fazia um fallback insensível a maiúsculas na hora de casar nome JSON com campo do struct. A v2 exige correspondência exata; jsonv2.MatchCaseInsensitiveNames(true) religa o comportamento antigo.

4. UTF-8 inválido é rejeitado

Strings com bytes inválidos passam a produzir erro em vez de caractere de substituição. Quem integra com sistema legado que emite CP1252 disfarçado de UTF-8 vai descobrir isso em produção, se não testar antes.

5. Slice nil vira [] e map nil vira {}

Talvez a mudança mais visível para o front-end:

Deixa de existir o null que obrigava o cliente TypeScript a escrever Array.isArray(x) ? x : [] . Para quem depende de null como sinal — "não carregado" versus "vazio" —, há jsonv2.FormatNilSliceAsNull(true) e FormatNilMapAsNull(true) .

Some a isso a semântica de omitempty , que na v2 também descarta struct vazia, enquanto omitzero (disponível desde o Go 1.24) olha o valor zero de fato, incluindo time.Time . Um relato de migração documentou um endpoint PATCH em que a mesma struct, com valor zero, passou a emitir {"count":0,"enabled":false} em vez de {} — ou seja, o patch "vazio" virou instrução de zerar dois campos. Em APIs com presença significativa, use ponteiro ou omitzero e trave o contrato com teste.

jsontext na prática: streaming sem reflection

O outro pacote resolve um problema antigo: processar JSON grande sem alocar o documento inteiro.

É o tipo de ferramenta que faltava para trilha de auditoria, mascaramento de dados sensíveis em log e transformação de payload de fila. Antes, o caminho era json.RawMessage e paciência, ou decodificar duas vezes.

Riscos que costumam passar batido

Erro muda de string. Mesmo sem trocar import, o v1 sobre o motor v2 pode mudar o texto do erro. Teste que compara err.Error() com string literal quebra no upgrade de versão, não na migração.

Contrato quebrado só no cliente. Nada no go vet avisa que seu slice nil virou [] . Golden files de payload (ou snapshot de resposta no teste de handler) pegam isso; revisão de PR não.

Marshal não ficou "de graça" mais rápido. Meça com os seus payloads antes de prometer ganho de latência.

Estrito demais na borda. Rejeitar campo desconhecido em endpoint que recebe dados de terceiro transforma compatibilidade futura em incidente. Escolha por endpoint, não por projeto.

Migração em três etapas

Suba para o Go 1.27 sem trocar imports. Rode a suíte e veja o que quebrou por mensagem de erro.

Compare v1 e v2 lado a lado nos structs de contrato: um teste que serializa com os dois pacotes e faz diff do JSON. É barato e revela todas as cinco diferenças de uma vez.

Troque por módulo, começando pelos endpoints de entrada onde a rigidez é vantagem, e deixe consumidores de webhook no v1 ou com DiscardUnknownMembers(true) .

Em projetos de porte médio, com serviços em Go conversando com back-ends em Python, Node.js e C# — combinação comum nos times da Maquinaweb —, esse contrato de JSON é justamente onde as incompatibilidades aparecem primeiro: null versus [] , campo numérico que chega como string, nome duplicado vindo de um serializador de terceiro. Vale tratar a troca de pacote como mudança de contrato de API, com o mesmo ritual de versionamento.

Meu recém-recomendação prática: adote a v2 nos módulos novos e nos handlers internos, mantenha a v1 onde o payload é de terceiro e coloque o teste de comparação no CI agora — não quando precisar mexer no código.

Se o seu time está avaliando migrar serviços Go, revisar contratos de API ou construir uma integração sob medida, dá para conversar com o time de engenharia da Maquinaweb e colocar essas decisões no papel antes da primeira linha de código.

Fontes

#Go #encoding/json #jsonv2 #API #breaking change #backend #migração
Contato

Bora conversar?

Três campos. A resposta vem no WhatsApp.

Ou escreva para contato@maquinaweb.com.br.

Você vai para o WhatsApp com a mensagem pronta. Sem spam, sem lista.

Seus dados seguem a nossa política de privacidade.