Encoding JSON
Nesta aula, você aprenderá a codificar e decodificar dados JSON em Go usando os pacotes padrão encoding/json. Exploraremos as funções Marshal e Unmarshal, o uso de tags de struct para controlar a serialização, campos opcionais e a diretiva omitempty. O conteúdo inclui exemplos práticos, boas práticas e exercícios para consolidar o conhecimento.
JSON (JavaScript Object Notation) é um formato de dados leve e amplamente utilizado para troca de informações entre sistemas. Em Go, o pacote encoding/json fornece ferramentas robustas para converter estruturas de dados em JSON (marshal) e vice-versa (unmarshal). Nesta aula, você dominará os conceitos essenciais para trabalhar com JSON em Go, incluindo o controle fino sobre a serialização via tags de struct.
Entender como manipular JSON é fundamental para desenvolver APIs, consumir serviços web e persistir dados. Vamos começar explorando as funções principais e, em seguida, aprofundar em técnicas para lidar com campos opcionais e omitir valores vazios.
Marshal e Unmarshal
A função json.Marshal converte um valor Go (como uma struct, mapa ou slice) em uma sequência de bytes JSON. Já json.Unmarshal faz o inverso: interpreta uma sequência de bytes JSON e preenche uma estrutura de dados Go. Essas funções são a base para qualquer operação de serialização em Go.
Vamos ver um exemplo simples de como usar Marshal para converter uma struct em JSON:
package main
import (
"encoding/json"
"fmt"
)
type Pessoa struct {
Nome string
Idade int
}
func main() {
p := Pessoa{Nome: "Maria", Idade: 30}
dados, err := json.Marshal(p)
if err != nil {
fmt.Println("Erro ao serializar:", err)
return
}
fmt.Println(string(dados)) // {"Nome":"Maria","Idade":30}
}Observe que os campos da struct são serializados com os nomes exatos (Nome, Idade). Para controlar o formato, usamos tags de struct, como veremos a seguir.
Para Unmarshal, precisamos passar um ponteiro para a variável que receberá os dados. Exemplo:
var p Pessoa
jsonData := []byte(`{"Nome":"João","Idade":25}`)
err := json.Unmarshal(jsonData, &p)
if err != nil {
fmt.Println("Erro ao desserializar:", err)
return
}
fmt.Printf("%+v\n", p) // {Nome:João Idade:25}É importante verificar os erros retornados, pois dados malformados ou incompatíveis de tipos causarão falhas.
Tags de struct
As tags de struct são anotações que permitem personalizar o comportamento de serialização do JSON. Elas são strings que seguem o campo e são interpretadas pelo pacote encoding/json. A tag mais comum é json:"nome_em_json", que define o nome da chave no JSON, diferente do nome do campo em Go.
Além do nome, podemos usar opções como omitempty e string. Vamos ver um exemplo:
type Produto struct {
Nome string `json:"nome"`
Preco float64 `json:"preco"`
Qtd int `json:"quantidade"`
}
func main() {
prod := Produto{Nome: "Caneta", Preco: 1.99, Qtd: 10}
dados, _ := json.Marshal(prod)
fmt.Println(string(dados)) // {"nome":"Caneta","preco":1.99,"quantidade":10}
}As tags são essenciais para garantir que o JSON produzido siga convenções como snake_case ou camelCase, e para ignorar campos com a tag json:"-".
Também podemos usar a opção string para forçar que o valor seja serializado como string, por exemplo:
type Exemplo struct {
Numero int `json:"numero,string"`
}
// Saída: {"numero":"123"}Campos opcionais
Em muitos cenários, nem todos os campos de uma estrutura precisam estar presentes no JSON. Em Go, campos ausentes no JSON são preenchidos com o valor zero do tipo (0 para int, "" para string, nil para ponteiros, etc.). Para distinguir entre um campo ausente e um campo com valor zero, podemos usar ponteiros ou o tipo json.RawMessage.
Uma abordagem comum é usar ponteiros para campos opcionais. Se o campo for um ponteiro, ele será nil quando ausente, permitindo verificar sua presença. Exemplo:
type Usuario struct {
Nome string `json:"nome"`
Email *string `json:"email,omitempty"`
}
func main() {
email := "user@example.com"
u := Usuario{Nome: "Ana", Email: &email}
dados, _ := json.Marshal(u)
fmt.Println(string(dados)) // {"nome":"Ana","email":"user@example.com"}
u2 := Usuario{Nome: "Bob"}
dados2, _ := json.Marshal(u2)
fmt.Println(string(dados2)) // {"nome":"Bob"} (sem email)
}Outra técnica é usar um mapa ou map[string]interface{} quando a estrutura é dinâmica, mas isso perde a segurança de tipos.
Durante o Unmarshal, se um campo não existir no JSON, ele permanecerá com o valor zero (ou nil para ponteiros). Isso permite que você trate campos opcionais de forma segura.
omitempty
A opção omitempty faz com que o campo seja omitido do JSON se ele tiver o valor zero para o tipo. Por exemplo, para uma string, omitempty omite se a string for vazia; para int, se for 0; para slice, se for nil ou vazio; para ponteiro, se for nil.
Isso é muito útil para não enviar campos desnecessários, reduzindo o tamanho do payload e mantendo o JSON limpo. Exemplo:
type Config struct {
Nome string `json:"nome"`
Porta int `json:"porta,omitempty"`
Debug bool `json:"debug,omitempty"`
}
func main() {
c1 := Config{Nome: "servidor"}
dados, _ := json.Marshal(c1)
fmt.Println(string(dados)) // {"nome":"servidor"}
c2 := Config{Nome: "servidor", Porta: 8080, Debug: true}
dados2, _ := json.Marshal(c2)
fmt.Println(string(dados2)) // {"nome":"servidor","porta":8080,"debug":true}
}Note que, no primeiro caso, os campos porta e debug foram omitidos por serem zero (0 e false).
É importante lembrar que omitempty não funciona em structs (a menos que sejam ponteiros para structs). Para campos do tipo struct, você precisa usar um ponteiro ou implementar a interface json.Marshaler.
Boas práticas e observações finais
Ao trabalhar com JSON em Go, siga estas boas práticas:
- Use tags de struct para definir nomes de campos claros e estáveis na API.
- Prefira ponteiros para campos opcionais, pois permitem distinguir ausência de valor zero.
- Aplique
omitemptypara evitar enviar campos desnecessários, mas tenha cuidado para não esconder valores intencionais zero. - Sempre verifique erros de
MarshaleUnmarshal. - Para APIs públicas, considere usar
json.Decoderejson.Encoderpara streaming.
Com esses conceitos, você está pronto para integrar JSON em seus programas Go de forma eficiente e robusta.
Exercícios
- Exercício 1: Crie uma struct
Livrocom campos Título, Autor e Ano. Use tags JSON para que os campos sejam serializados comotitulo,autoreano. Escreva um programa que serialize uma instância para JSON e imprima o resultado. - Exercício 2: Escreva uma função que receba um JSON (como
[]byte) e preencha uma structPessoacom campos Nome e Idade. UseUnmarshale imprima os valores. - Exercício 3: Crie uma struct
Enderecocom campos Rua, Cidade e CEP. Use tags JSON e apliqueomitemptynos campos opcionais (Rua e Cidade podem ser vazios). Teste a serialização com e sem valores. - Exercício 4: Defina uma struct
Produtocom campos Nome, Preco e Quantidade. Use a tagjson:"preco,string"para que o preço seja serializado como string. Serialize e verifique a saída. - Exercício 5: Implemente um campo opcional usando ponteiro para string. Crie uma struct
Pedidocom ID (int) e Observação (*string). Serialize um pedido sem observação e outro com observação, e observe a diferença.
type Livro struct {
Título string `json:"titulo"`
Autor string `json:"autor"`
Ano int `json:"ano"`
}
func main() {
livro := Livro{Título: "Dom Casmurro", Autor: "Machado de Assis", Ano: 1899}
dados, _ := json.Marshal(livro)
fmt.Println(string(dados)) // {"titulo":"Dom Casmurro","autor":"Machado de Assis","ano":1899}
}type Pessoa struct {
Nome string `json:"nome"`
Idade int `json:"idade"`
}
func decodificarJSON(dados []byte) {
var p Pessoa
err := json.Unmarshal(dados, &p)
if err != nil {
fmt.Println("Erro:", err)
return
}
fmt.Printf("Nome: %s, Idade: %d\n", p.Nome, p.Idade)
}
func main() {
dados := []byte(`{"nome":"Carlos","idade":40}`)
decodificarJSON(dados)
}type Endereco struct {
Rua string `json:"rua,omitempty"`
Cidade string `json:"cidade,omitempty"`
CEP string `json:"cep"`
}
func main() {
e1 := Endereco{CEP: "12345-678"}
dados, _ := json.Marshal(e1)
fmt.Println(string(dados)) // {"cep":"12345-678"}
e2 := Endereco{Rua: "Av. Paulista", Cidade: "São Paulo", CEP: "01310-100"}
dados2, _ := json.Marshal(e2)
fmt.Println(string(dados2)) // {"rua":"Av. Paulista","cidade":"São Paulo","cep":"01310-100"}
}type Produto struct {
Nome string `json:"nome"`
Preco float64 `json:"preco,string"`
Quantidade int `json:"quantidade"`
}
func main() {
p := Produto{Nome: "Mouse", Preco: 49.90, Quantidade: 5}
dados, _ := json.Marshal(p)
fmt.Println(string(dados)) // {"nome":"Mouse","preco":"49.9","quantidade":5}
}type Pedido struct {
ID int `json:"id"`
Observacao *string `json:"observacao,omitempty"`
}
func main() {
obs := "Entregar após 18h"
p1 := Pedido{ID: 1}
dados1, _ := json.Marshal(p1)
fmt.Println(string(dados1)) // {"id":1}
p2 := Pedido{ID: 2, Observacao: &obs}
dados2, _ := json.Marshal(p2)
fmt.Println(string(dados2)) // {"id":2,"observacao":"Entregar após 18h"}
}