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 omitempty para evitar enviar campos desnecessários, mas tenha cuidado para não esconder valores intencionais zero.
  • Sempre verifique erros de Marshal e Unmarshal.
  • Para APIs públicas, considere usar json.Decoder e json.Encoder para streaming.

Com esses conceitos, você está pronto para integrar JSON em seus programas Go de forma eficiente e robusta.

Exercícios

  1. Exercício 1: Crie uma struct Livro com campos Título, Autor e Ano. Use tags JSON para que os campos sejam serializados como titulo, autor e ano. Escreva um programa que serialize uma instância para JSON e imprima o resultado.
  2. ✓ Resposta:
    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}
    }
  3. Exercício 2: Escreva uma função que receba um JSON (como []byte) e preencha uma struct Pessoa com campos Nome e Idade. Use Unmarshal e imprima os valores.
  4. ✓ Resposta:
    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)
    }
  5. Exercício 3: Crie uma struct Endereco com campos Rua, Cidade e CEP. Use tags JSON e aplique omitempty nos campos opcionais (Rua e Cidade podem ser vazios). Teste a serialização com e sem valores.
  6. ✓ Resposta:
    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"}
    }
  7. Exercício 4: Defina uma struct Produto com campos Nome, Preco e Quantidade. Use a tag json:"preco,string" para que o preço seja serializado como string. Serialize e verifique a saída.
  8. ✓ Resposta:
    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}
    }
  9. Exercício 5: Implemente um campo opcional usando ponteiro para string. Crie uma struct Pedido com ID (int) e Observação (*string). Serialize um pedido sem observação e outro com observação, e observe a diferença.
  10. ✓ Resposta:
    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"}
    }

Referências