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

Exercícios

  1. Defina uma struct Livro com campos Titulo, Autor, Ano (int) e Editora (string). Use tags para que os nomes em JSON sejam minúsculos. Crie uma instância e serialize para JSON.
  2. ✓ Resposta:
    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"}
    }
  3. Dado o JSON {"nome":"João","salario":5000.00,"cargo":"Desenvolvedor"}, crie uma struct Funcionario com campos apropriados e faça unmarshal. Depois, imprima os campos.
  4. ✓ Resposta:
    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)
    }
  5. Crie uma struct Endereco com campos Rua, Numero (opcional, use ponteiro) e Cidade. Faça unmarshal de um JSON que não contenha Numero e verifique que ele é nil.
  6. ✓ Resposta:
    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
    }
  7. Defina uma struct Pedido com campos ID, Cliente, Total e Desconto (float64). Use a tag omitempty em Desconto para que seja omitido quando for 0. Serialize um pedido sem desconto e outro com desconto.
  8. ✓ Resposta:
    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}
    }
  9. Escreva um programa que leia um JSON de um arquivo (simule com uma string) contendo uma lista de produtos (cada um com nome e preco) e calcule o preço total. Use um slice de struct com tags.
  10. ✓ Resposta:
    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
    }