Cuidados Essenciais em Go: Como utilizar as Funções Marshal() e Unmarshal() para JSON de maneira segura
Recentemente o tech lead da minha equipe encontrou um bug envolvendo o Unmarshal()
em Go, ele mostrou pra gente o que aconteceu e então conversamos sobre o risco daquilo acontecer. Isso acabou me deixando intrigado e acabei pesquisando mais sobre o que pode acontecer quando não tomamos os devidos cuidados ao manipular o JSON em um código feito em Go.
Sendo assim, vamos começar falando um pouco o que são as funções de Marshal()
e Unmarshal()
em Go e em seguida vou mostrar erros que podem acontecer caso não tomemos os devidos cuidados. Vamos lá?
Manipulação nativa do JSON em GO: Marshal e Unmarshal
Quando se trata de manipulação de dados em formato JSON na linguagem de programação Go, as funções Marshal()
e Unmarshal()
são ferramentas poderosas que permitem a conversão entre uma struct (ou objetos, caso queira chamar assim) em JSON bem como JSON em uma struct respectivamente. No entanto, é importante compreender os perigos potenciais associados a essas operações e adotar algumas práticas para evitar erros sutis e que pode te salvar algumas horas de debugging atrás de um erro.
Erro 1: Esquecendo de exportar os campos
Começando então com erros comuns, esse é até simples de resolver, mas pode dar um trabalhinho pra achar se não tomar cuidado. Lembrando que Go trabalha com privacidade de uma variavel, função, método ou campo se a primeira letra é minúscula ou maiúscula, indicando que é privado ou público respectivamente. Pois bem, ao trabalhar com Marshal()
e Unmarshal()
, lembre-se de que somente campos exportados, ou seja, públicos (iniciando com letra maiúscula) em uma estrutura serão considerados. Campos não exportados não serão incluídos na serialização (convertendo o objeto/struct em JSON) ou serão preenchidos com valores zero durante a desserialização (convertendo o JSON em objeto/struct.
Bom vamos avaliar o erro simples aqui nesse caso, beleza? Temos o objeto Usuario, onde temos dois campos: NomeDeUsuario e senha, um sendo público e o outro privado. Por conta disso, ao tentar serializar, o JSON não terá o campo senha e ao desserializar podemos ver que o campo senha não é preenchido:
erro_campo_nao_exportado.go:
package main
import (
"encoding/json"
"fmt"
)
type Usuario struct {
NomeDeUsuario string `json:"nome_de_usuario"`
senha string `json:"senha"` // campo não exportado
}
func main() {
// Caso de serialização
jose := Usuario{NomeDeUsuario: "jose", senha: "senha-do-jose"}
stringJson, errMarshal := json.Marshal(jose) // O campo 'senha' não será incluído por conta que ele é privado
if errMarshal != nil {
fmt.Println(errMarshal.Error())
panic(errMarshal)
}
fmt.Println("json do jose: " + string(stringJson))
// Caso de desserialização
var maria Usuario
jsonMaria := []byte("{\\"nome_de_usuario\\":\\"maria\\", \\"senha\\":\\"senha-de-maria\\"}")
errUnamarshal := json.Unmarshal(jsonMaria, &maria)
if errUnamarshal != nil {
fmt.Println(errUnamarshal.Error())
panic(errUnamarshal)
}
fmt.Println(fmt.Sprintf("struc de maria: %+v", maria))
}
Ao executar o comando go run erro_campo_nao_exportado.go
podemos ver a seguinte saída no console:
json do jose: {"nome_de_usuario":"jose"}
struc de maria: {NomeDeUsuario:maria senha:}
Então para evitar alguns minutos, e até horas, tentando procurar porque não salvou um determinado campos, vamos sempre certificar de que apenas os campos que devem ser serializados estejam exportados.
solucao_erro_campo_nao_exportado.go:
package main
import (
"encoding/json"
"fmt"
)
type Usuario struct {
NomeDeUsuario string `json:"nome_de_usuario"`
Senha string `json:"senha"` // campo não exportado
}
func main() {
// Caso de serialização
jose := Usuario{NomeDeUsuario: "jose", Senha: "senha-do-jose"}
stringJson, errMarshal := json.Marshal(jose) // O campo 'senha' não será incluído por conta que ele é privado
if errMarshal != nil {
fmt.Println(errMarshal.Error())
panic(errMarshal)
}
fmt.Println("json do jose: " + string(stringJson))
// Caso de desserialização
var maria Usuario
jsonMaria := []byte("{\\"nome_de_usuario\\":\\"maria\\", \\"senha\\":\\"senha-de-maria\\"}")
errUnamarshal := json.Unmarshal(jsonMaria, &maria)
if errUnamarshal != nil {
fmt.Println(errUnamarshal.Error())
panic(errUnamarshal)
}
fmt.Println(fmt.Sprintf("struc de maria: %+v", maria))
}
E ao executar solução, teremos a seguinte saída:
json do jose: {"nome_de_usuario":"jose"}
struc de maria: {NomeDeUsuario:maria senha:}
Erro 2: Não validando dados de entrada para o Unmarshal()
Bom, este segundo erro foi algo que vi recentemente, e na hora lembrei que já passei por isso um bom tempo atrás, no artigo de Bahar Shah no Medium: Go’s unmarshal errors might not work the way you think they do. O título é bem autoexplicativo e faz todo sentido, no Unmarshal()
o erro que é retornado é apenas se o JSON for mal formado, ele não valida os campos. Então se você receber um JSON qualquer, mesmo que ele não tenha nenhum campo da sua struct, a dessarialização é considaerada como um sucesso, isso quer dizer sem erros, mas o seu objeto/struct não virá preenchido com valor algum.
Então antes de usar a função Unmarshal()
, é importante que validemos os dados de entrada para evitar erros vindo do JSON. Para fazer validação existem algumas práticas no Go, como: você pode criar sua própria função de validação 100% do zero, ou pode criar a validação usando o pacote Validator onde nós conseguiremos facilitar as validações usando uma forma de simplificar como declarar e como validar. Vamos ao exemplo, usando o Validator pois é uma solução que pode ser expandida em algo mais completo, beleza?
Nosso código recebe um JSON contendo informações sobre cachorro, certo? Nessa validação, nosso objeto tem 3 campos: Nome, Idade e EhAmigavel onde cada uma tem sua propria validação. O Nome por exemplo ele é obrigatório (required) e precisa ter no mínimo 3 caracteres e no máximo 12 caracteres (min=3,max=12), já a Idade ela é obrigatórias e precisa ser um número (numeric) e por final o EhAmivavel é apenas obrigatório. Agora imaginemos que sem querer recebemos os dados do cachorro, mas faltando um campo. Nesse caso com a validação nós vamos evitar de salvar os dados incompletos nuam base por exemplo. Vamos ver o código abaixo com erro:
erro_nao_validando_campos.go:
package main
import (
"encoding/json"
"errors"
"fmt"
"github.com/go-playground/validator/v10"
)
type Cachorro struct {
Nome string `json:"nome" validate:"required,min=3,max=12"`
Idade int `json:"idade" validate:"required,numeric"`
EhAmigavel bool `json:"eh_amigavel" validate:"required"`
}
type ErrosDeValidacao struct {
Campo string
Tag string
Valor string
}
var Validator = validator.New()
func validaCachorro(cachorro Cachorro) []ErrosDeValidacao {
erros := make([]ErrosDeValidacao, 0)
err := Validator.Struct(cachorro)
if err != nil {
for _, erroValidator := range err.(validator.ValidationErrors) {
erro := ErrosDeValidacao{
Campo: erroValidator.Field(),
Tag: erroValidator.Tag(),
Valor: erroValidator.Param(),
}
erros = append(erros, erro)
}
return erros
}
return nil
}
func main() {
jsonCachorro := []byte("{\\"nome\\":\\"bilu\\", \\"idade\\":5}")
var cachorro Cachorro
errUnamarshal := json.Unmarshal(jsonCachorro, &cachorro)
if errUnamarshal != nil {
fmt.Println(errUnamarshal.Error())
panic(errUnamarshal)
}
errosValidacao := validaCachorro(cachorro)
if len(errosValidacao) > 0 {
fmt.Println(fmt.Sprintf("erros: %+v", errosValidacao))
panic(errors.New("erro ao validar o json do cachorro"))
}
fmt.Println(fmt.Sprintf("cachorro: %+v", cachorro))
}
Ao executar o código com go run erro_nao_validando_campos.go
, temos a seguinte saída:
erros: [{Campo:EhAmigavel Tag:required Valor:}]
panic: erro ao validar o json do cachorro
goroutine 1 [running]:
main.main()
caminho/para/seu/codigo/erro_nao_validando_campos.go:54
exit status 2
Agora se executamos o mesmo código, com a seguinte correção no json:
jsonCachorro := []byte("{\\"nome\\":\\"bilu\\", \\"idade\\":5, \\"eh_amigavel\\":true}")
E executarmos novamente o código, nossa saída será a seguinte abaixo, mostrando que a validação funciona e vai barrar dados errados:
cachorro: {Nome:bilu Idade:5 EhAmigavel:true}
Erro 3: Loops Infinitos em Referências Circulares ao fazer Marshal!
Agora um erro que pode acontecer, caso você tenha que lidar com referencias circulares no código (algo bem específico, diga-se de passagem, como manipular uma árvore binária) é o de um loop infinito ao chamar a função Marshal()
ou Unmarshal()
, o que pode ocasionar um travamento no programa ou causando um consumo excessivo de recursos.
Tá, talvez você tenha tentado imaginar um cenário que isso ocorra certo? Bem, vou simplificar o máximo que consigo: Imagino um cenário onde você está fazendo uma rede social e você precisa devolver os dados da Maria, que tem o João na lista de amigos. Sendo assim, você buscou os dados de Maria e João na base de dados e agora vai preencher a relação de amizade deles e posteriormente vai transformar o objeto que representa a Maria em um slice de bytes (array de bytes do Go) para poder devolver ele na sua API, já que o Go vai converter esses dados numa string de resposta no endpoint. Se você ficou confuso com essa conversão aqui de objeto para slice de bytes pra devolver na API, não se preocupe que um outro artigo irei explicar sobre isso.
Pois bem, vamos ao exemplo de código com o erro a seguir
rede_social_loop_infinito_marshal.go:
package main
import (
"encoding/json"
"fmt"
)
type Pessoa struct {
Nome string `json:"nome"`
Amigos []*Pessoa `json:"amigos"`
}
func main() {
maria := &Pessoa{Nome: "Maria"}
joao := &Pessoa{Nome: "João"}
maria.Amigos = []*Pessoa{joao}
joao.Amigos = []*Pessoa{maria}
data, err := json.Marshal(maria) // Pode entrar em um ciclo infinito aqui oh!
if err != nil {
fmt.Println(err.Error())
panic(err)
}
fmt.Println(string(data))
}
Ao executar o código acima com o go run rede_social_loop_infinito_marshal.go
vamos ver que o Go é preparado para detectar esse loop e então ele vai retornar um erro para nós. A saída será assim:
json: unsupported value: encountered a cycle via *main.Pessoa
panic: json: unsupported value: encountered a cycle via *main.Pessoa
goroutine 1 [running]:
main.main()
caminho/para/seu/codigo/rede_social_loop_infinito_marshal.go:22
exit status 2
Bom, agora vamos solucionar o problema que geramos! Para solucionar isso podemos fazer de duas formas. A primeira é pedir para que aquele campo seja ignorado no JSON, e fazemos isso na declaração do campo usando json:"-"
no lugar de json:"amigos"
. Mas isso faria com que não enviássemos a lista de amigos e nós queremos enviar, certo? Pois bem, vamos resolver de outra forma, que é usando um formato intermediário para a serialização. No geral esse formato vai ser mais simplificado. Vamos ao código abaixo.
solucao_rede_social_loop_infinito_marshal.go:
package main
import (
"encoding/json"
"fmt"
)
type Pessoa struct {
Nome string `json:"nome"`
Amigos []*Pessoa `json:"amigos"`
}
type PessoaSerializavel struct {
Nome string `json:"nome"`
Amigos []string `json:"amigos"`
}
func convertaParaPessoaSerializavel(pessoa *Pessoa) PessoaSerializavel {
amigosSerializaveis := make([]string, len(pessoa.Amigos))
for indice, amigo := range pessoa.Amigos {
amigosSerializaveis[indice] = amigo.Nome
}
return PessoaSerializavel{
Nome: pessoa.Nome,
Amigos: amigosSerializaveis,
}
}
func main() {
maria := &Pessoa{Nome: "Maria"}
joao := &Pessoa{Nome: "João"}
maria.Amigos = []*Pessoa{joao}
joao.Amigos = []*Pessoa{maria}
data, err := json.Marshal(convertaParaPessoaSerializavel(maria)) // agora aqui não dará mais erro de loop
if err != nil {
fmt.Println(err.Error())
panic(err)
}
fmt.Println(string(data))
}
A solução aqui funciona bem ao executarmos o código com um go run solucao_rede_social_loop_infinito_marshal.go
. Sei que é um código forçado, que conseguiríamos evitar esse problema de outra forma, mas foi assim que consegui pensar da maneira mais simples para analisar este erro. É meramente didático e espero que tenha conseguido passar o erro e como corrigi-lo.
Conclusão
Bem, chegamos ao final desse artigo! Trouxe este artigo com erros e forma de corrigir após iniciar a leitura do 100 Go Mistakes and How to Avoid Them onde o autor do livro começa trazendo erros e más práticas e depois tras soluções para estes casos que ele apresenta, então decidi tentar o mesmo modelo com algo mais básico para ver como funcionaria, espero que gostem!
E agora sim, para concluir de fato, as funções Marshal()
e Unmarshal()
são recursos cruciais ao trabalhar com JSON em Go, mas demandam cuidados específicos para garantir que seu código seja eficiente, seguro e livre de erros. Ao estar ciente dos perigos potenciais e seguir algumas das práticas que comentei aqui, você poderá utilizar essas funções de maneira eficaz e sem muitas preocupações.
Mas lembre-se sempre, podem (e devem) existir outros erros que não comentei aqui, então sempre deverá consultar a documentação oficial do pacote encoding/json
para obter informações detalhadas sobre o uso correto dessas funções, com exemplos, bem como sempre praticar e conversar com outras pessoas que programem em Go para saber com elas fazem e aprender com elas.
Referências
- Donovan, A. A., & Kernighan, B. W. (2016). A Linguagem de Programação Go. Novatec Editora.
- Página do pacote encoding/json. Disponível em: https://pkg.go.dev/encoding/json.
- Stackoverflow. Disponível em: https://stackoverflow.com/questions/32708717/go-when-will-json-unmarshal-to-struct-return-error
- Artigo Medium de Bahar Shah - Go’s unmarshal errors might not work the way you think they do. Disponível em: https://baharbshah.medium.com/gos-unmarshal-errors-might-not-work-the-way-you-think-they-do-949f8fe15a09
- Página do pacote Validator. Disponível em: https://pkg.go.dev/github.com/go-playground/validator/v10