Cuidados Essenciais em Go: Como utilizar as Funções Marshal() e Unmarshal() para JSON de maneira segura
Tags: beginners, go, programming, braziliandevs
Published: 2023-09-09 00:29:38

Recentemente o tech lead da minha equipe encontrou um bug envolvendo o `**Unhmarshal()**` 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:**

```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:

```bash
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:**

```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:

```bash
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:**

```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:

```bash
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:

```go
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:

```bash
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:**

```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:

```bash
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:**

```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`](https://pkg.go.dev/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