Encoding JSON
Nesta aula, você aprenderá a trabalhar com JSON em Go, incluindo marshal/unmarshal, uso de tags de struct, campos opcionais e a opção omitempty. O conteúdo aborda desde conceitos básicos até boas práticas para serialização e desserialização eficientes.
JSON (JavaScript Object Notation) é um formato leve de troca de dados amplamente utilizado em APIs web e arquivos de configuração. Em Go, a biblioteca padrão encoding/json fornece funções poderosas para converter dados entre Go e JSON. Nesta aula, exploraremos os mecanismos de marshal (conversão de Go para JSON) e unmarshal (conversão de JSON para Go), além de técnicas avançadas como tags de struct, campos opcionais e a opção omitempty. Dominar esses conceitos é essencial para construir aplicações Go que interajam com serviços web de forma eficiente e segura.
Entender como controlar a serialização é fundamental: você pode personalizar nomes de campos, ignorar campos vazios, lidar com dados ausentes e até mesmo validar entradas. Vamos começar com os fundamentos e depois mergulhar em exemplos práticos.
Marshal e Unmarshal
Marshal é o processo de converter uma estrutura Go (como structs, slices, maps) em uma string JSON. A função json.Marshal retorna um slice de bytes ([]byte) contendo o JSON, ou um erro se a conversão falhar. Unmarshal faz o inverso: converte um slice de bytes JSON em uma estrutura Go. A função json.Unmarshal modifica o valor apontado pelo segundo argumento (que deve ser um ponteiro) com os dados desserializados.
Exemplo básico:
package main
import (
"encoding/json"
"fmt"
)
type Pessoa struct {
Nome string
Idade int
}
func main() {
// Marshal
p := Pessoa{Nome: "João", Idade: 30}
bytes, err := json.Marshal(p)
if err != nil {
panic(err)
}
fmt.Println(string(bytes)) // {"Nome":"João","Idade":30}
// Unmarshal
jsonStr := `{"Nome":"Maria","Idade":25}`
var p2 Pessoa
err = json.Unmarshal([]byte(jsonStr), &p2)
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", p2) // {Nome:Maria Idade:25}
}
Observe que, por padrão, os nomes dos campos em JSON são iguais aos nomes dos campos na struct (com a primeira letra maiúscula). Para personalizar, usamos tags.
Tags de struct
Tags de struct são metadados anexados aos campos de uma struct, escritos entre crases (`). No contexto de JSON, elas permitem controlar como o campo é serializado/desserializado, como alterar o nome, ignorar campos ou definir comportamento especial. A tag é uma string no formato json:"nome".
Exemplo:
type Produto struct {
ID int `json:"id"`
Nome string `json:"nome"`
Preco float64 `json:"preco"`
}
func main() {
prod := Produto{ID: 1, Nome: "Caneta", Preco: 2.50}
bytes, _ := json.Marshal(prod)
fmt.Println(string(bytes)) // {"id":1,"nome":"Caneta","preco":2.5}
}
Sem a tag, o JSON teria chaves como "ID", "Nome", "Preco". Com a tag, usamos minúsculas. Tags também podem conter opções separadas por vírgula, como omitempty e string.
Campos opcionais
Em muitos cenários, um campo JSON pode estar ausente, mas ainda queremos representá-lo na struct. Para campos opcionais, usamos ponteiros ou tipos que aceitam nil. Por exemplo, se um campo pode não estar presente no JSON, declare-o como um ponteiro (*Tipo) ou use tipos como json.RawMessage para processamento adiado.
Exemplo com ponteiro:
type Config struct {
Nome string `json:"nome"`
Valor *string `json:"valor,omitempty"`
}
func main() {
jsonStr := `{"nome":"teste"}`
var c Config
json.Unmarshal([]byte(jsonStr), &c)
fmt.Printf("Nome: %s, Valor: %v\n", c.Nome, c.Valor) // Valor é nil
jsonStr2 := `{"nome":"teste","valor":"abc"}`
var c2 Config
json.Unmarshal([]byte(jsonStr2), &c2)
fmt.Printf("Nome: %s, Valor: %s\n", c2.Nome, *c2.Valor)
}
Outra abordagem é usar interface{} ou tipos personalizados que implementam json.Unmarshaler. Para campos que podem ter tipos diferentes, use json.RawMessage para adiar a desserialização.
omitempty
A opção omitempty instrui o marshal a omitir o campo do JSON se ele tiver seu valor zero (para tipos numéricos: 0, string: "", slice/map: nil, etc.). É útil para evitar campos vazios desnecessários.
Exemplo:
type Usuario struct {
Nome string `json:"nome"`
Email string `json:"email,omitempty"`
Idade int `json:"idade,omitempty"`
}
func main() {
u1 := Usuario{Nome: "Ana"}
bytes, _ := json.Marshal(u1)
fmt.Println(string(bytes)) // {"nome":"Ana"}
u2 := Usuario{Nome: "Ana", Email: "ana@teste.com", Idade: 0}
bytes2, _ := json.Marshal(u2)
fmt.Println(string(bytes2)) // {"nome":"Ana","email":"ana@teste.com"} (idade omitida)
}
Note que omitempty funciona apenas na serialização (marshal). Durante o unmarshal, campos ausentes no JSON mantêm o valor zero da struct, independentemente da tag.
Boas práticas e observações finais
Sempre trate erros de marshal/unmarshal, especialmente ao lidar com dados externos. Use json.Decoder para streams grandes (arquivos ou requisições HTTP) em vez de carregar tudo na memória. Evite usar interface{} a menos que necessário, pois perde a segurança de tipos. Prefira structs bem definidas com tags claras. Ao consumir APIs que podem enviar campos inesperados, considere usar json.RawMessage ou json.Decoder.DisallowUnknownFields() para rejeitar campos desconhecidos.
Referências
- Documentação oficial do pacote encoding/json
- Go Blog: JSON and Go
- Soham Kamani: Comprehensive Guide to JSON in Go
- Medium: Working with JSON in Go
- Go Wiki: JSON
- Stack Overflow: Go JSON Questions
Exercícios
- Defina uma struct
Livrocom camposTitulo,Autor,Ano(int) eEditora(string). Use tags para que os nomes em JSON sejam minúsculos. Crie uma instância e serialize para JSON. - Dado o JSON
{"nome":"João","salario":5000.00,"cargo":"Desenvolvedor"}, crie uma structFuncionariocom campos apropriados e faça unmarshal. Depois, imprima os campos. - Crie uma struct
Enderecocom camposRua,Numero(opcional, use ponteiro) eCidade. Faça unmarshal de um JSON que não contenhaNumeroe verifique que ele é nil. - Defina uma struct
Pedidocom camposID,Cliente,TotaleDesconto(float64). Use a tagomitemptyemDescontopara que seja omitido quando for 0. Serialize um pedido sem desconto e outro com desconto. - Escreva um programa que leia um JSON de um arquivo (simule com uma string) contendo uma lista de produtos (cada um com
nomeepreco) e calcule o preço total. Use um slice de struct com tags.
type Livro struct {
Titulo string `json:"titulo"`
Autor string `json:"autor"`
Ano int `json:"ano"`
Editora string `json:"editora"`
}
func main() {
livro := Livro{Titulo: "1984", Autor: "George Orwell", Ano: 1949, Editora: "Companhia das Letras"}
bytes, _ := json.Marshal(livro)
fmt.Println(string(bytes))
// Saída: {"titulo":"1984","autor":"George Orwell","ano":1949,"editora":"Companhia das Letras"}
}
type Funcionario struct {
Nome string `json:"nome"`
Salario float64 `json:"salario"`
Cargo string `json:"cargo"`
}
func main() {
jsonStr := `{"nome":"João","salario":5000.00,"cargo":"Desenvolvedor"}`
var f Funcionario
err := json.Unmarshal([]byte(jsonStr), &f)
if err != nil {
panic(err)
}
fmt.Printf("Nome: %s, Salário: %.2f, Cargo: %s\n", f.Nome, f.Salario, f.Cargo)
}
type Endereco struct {
Rua string `json:"rua"`
Numero *int `json:"numero,omitempty"`
Cidade string `json:"cidade"`
}
func main() {
jsonStr := `{"rua":"Av. Paulista","cidade":"São Paulo"}`
var e Endereco
json.Unmarshal([]byte(jsonStr), &e)
fmt.Printf("Rua: %s, Numero: %v, Cidade: %s\n", e.Rua, e.Numero, e.Cidade)
// Numero será nil
}
type Pedido struct {
ID int `json:"id"`
Cliente string `json:"cliente"`
Total float64 `json:"total"`
Desconto float64 `json:"desconto,omitempty"`
}
func main() {
p1 := Pedido{ID: 1, Cliente: "João", Total: 100.0}
bytes1, _ := json.Marshal(p1)
fmt.Println(string(bytes1)) // {"id":1,"cliente":"João","total":100}
p2 := Pedido{ID: 2, Cliente: "Maria", Total: 200.0, Desconto: 10.0}
bytes2, _ := json.Marshal(p2)
fmt.Println(string(bytes2)) // {"id":2,"cliente":"Maria","total":200,"desconto":10}
}
type Produto struct {
Nome string `json:"nome"`
Preco float64 `json:"preco"`
}
func main() {
jsonStr := `[{"nome":"Caneta","preco":2.5},{"nome":"Caderno","preco":15.0}]`
var produtos []Produto
err := json.Unmarshal([]byte(jsonStr), &produtos)
if err != nil {
panic(err)
}
total := 0.0
for _, p := range produtos {
total += p.Preco
}
fmt.Printf("Total: %.2f\n", total) // Total: 17.50
}