Artigo 27 — Trabalhando com JSON: encode, decode e tags de struct
Curso: Dominando Go em 1 Ano Prof. Ricardo Matos Módulo 5 — I/O, HTTP e APIs
JSON: a língua franca das APIs modernas
JSON tornou-se o formato padrão de troca de dados entre sistemas. APIs REST, arquivos de configuração, mensagens em filas, respostas de webhooks — praticamente todo sistema moderno produz ou consome JSON em algum ponto. Go oferece suporte nativo e completo através do pacote encoding/json, com uma API que equilibra simplicidade, flexibilidade e performance.
O pacote usa reflection para inspecionar structs em tempo de execução e mapear campos automaticamente. Isso significa que a maior parte do trabalho de serialização e desserialização acontece sem código manual — basta definir a struct corretamente.
Conceitos fundamentais: Marshal e Unmarshal
Marshal converte um valor Go em JSON. Unmarshal faz o caminho inverso — converte JSON em um valor Go:
package main
import (
"encoding/json"
"fmt"
)
type Produto struct {
ID int `json:"id"`
Nome string `json:"nome"`
Preco float64 `json:"preco"`
Estoque int `json:"estoque"`
Ativo bool `json:"ativo"`
}
func main() {
// Go → JSON (Marshal)
p := Produto{
ID: 1,
Nome: "Teclado Mecânico",
Preco: 349.90,
Estoque: 42,
Ativo: true,
}
dados, err := json.Marshal(p)
if err != nil {
fmt.Println("Erro ao serializar:", err)
return
}
fmt.Println(string(dados))
// {"id":1,"nome":"Teclado Mecânico","preco":349.9,"estoque":42,"ativo":true}
// JSON → Go (Unmarshal)
jsonStr := `{"id":2,"nome":"Mouse Gamer","preco":189.90,"estoque":15,"ativo":true}`
var p2 Produto
if err := json.Unmarshal([]byte(jsonStr), &p2); err != nil {
fmt.Println("Erro ao deserializar:", err)
return
}
fmt.Printf("%+v\n", p2)
// {ID:2 Nome:Mouse Gamer Preco:189.9 Estoque:15 Ativo:true}
}
Tags de struct: controlando a serialização
As tags json nos campos da struct controlam como cada campo é serializado e deserializado. A sintaxe completa é json:"nome,opções":
type Usuario struct {
ID int `json:"id"`
Nome string `json:"nome"`
Email string `json:"email"`
Senha string `json:"-"` // sempre omitido
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at,omitempty"` // omitido se zero
Apelido string `json:"apelido,omitempty"` // omitido se vazio
Interno string `json:",omitempty"` // usa nome do campo, omite se vazio
Debug string `json:"-,"` // nome literal é "-"
}
As opções mais importantes:
omitempty— omite o campo se for o valor zero do tipo:0,"",false,nil, slice vazio, map vazio-— sempre omite o campo da serializaçãostring— serializa números e bools como strings JSON
type Pagamento struct {
Valor float64 `json:"valor,string"` // serializa como "349.90" em vez de 349.90
Parcelas int `json:"parcelas,string"`
}
p := Pagamento{Valor: 349.90, Parcelas: 3}
dados, _ := json.Marshal(p)
fmt.Println(string(dados)) // {"valor":"349.9","parcelas":"3"}
MarshalIndent: JSON formatado para leitura humana
dados, err := json.MarshalIndent(produto, "", " ")
if err != nil {
fmt.Println("Erro:", err)
return
}
fmt.Println(string(dados))
// {
// "id": 1,
// "nome": "Teclado Mecânico",
// "preco": 349.9,
// "estoque": 42,
// "ativo": true
// }
O segundo argumento é o prefixo de cada linha — normalmente vazio. O terceiro é o recuo por nível — convencionalmente dois ou quatro espaços.
Tipos complexos: slices, maps e structs aninhadas
JSON aninhado mapeia naturalmente para structs aninhadas e slices:
package main
import (
"encoding/json"
"fmt"
)
type Endereco struct {
Rua string `json:"rua"`
Cidade string `json:"cidade"`
Estado string `json:"estado"`
CEP string `json:"cep"`
}
type Contato struct {
Tipo string `json:"tipo"` // "email", "telefone"
Valor string `json:"valor"`
}
type Cliente struct {
ID int `json:"id"`
Nome string `json:"nome"`
Endereco Endereco `json:"endereco"`
Contatos []Contato `json:"contatos"`
Tags []string `json:"tags,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
func main() {
cliente := Cliente{
ID: 1,
Nome: "Empresa Tech Ltda",
Endereco: Endereco{
Rua: "Av. Paulista, 1000",
Cidade: "São Paulo",
Estado: "SP",
CEP: "01310-100",
},
Contatos: []Contato{
{Tipo: "email", Valor: "contato@tech.com"},
{Tipo: "telefone", Valor: "(11) 3000-0000"},
},
Tags: []string{"premium", "ativo"},
Metadata: map[string]string{
"segmento": "tecnologia",
"regiao": "sudeste",
},
}
dados, _ := json.MarshalIndent(cliente, "", " ")
fmt.Println(string(dados))
// Deserializando de volta
var c2 Cliente
json.Unmarshal(dados, &c2)
fmt.Println(c2.Contatos[0].Valor) // contato@tech.com
}
Encoder e Decoder: streaming de JSON
Para I/O com arquivos, conexões de rede e requisições HTTP, json.Encoder e json.Decoder trabalham diretamente com io.Writer e io.Reader, evitando a alocação de buffers intermediários:
package main
import (
"encoding/json"
"fmt"
"os"
"strings"
)
type Evento struct {
Tipo string `json:"tipo"`
Payload string `json:"payload"`
TS int64 `json:"ts"`
}
func main() {
// Encoder — escreve diretamente em qualquer Writer
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ") // equivalente ao MarshalIndent
eventos := []Evento{
{Tipo: "login", Payload: "user_42", TS: 1710000000},
{Tipo: "compra", Payload: "pedido_1001", TS: 1710000060},
{Tipo: "logout", Payload: "user_42", TS: 1710000120},
}
for _, e := range eventos {
encoder.Encode(e) // cada Encode escreve um objeto JSON + newline
}
// Decoder — lê de qualquer Reader
// Útil para processar múltiplos objetos JSON em sequência (NDJSON)
ndjson := `{"tipo":"ping","payload":"","ts":1710000180}
{"tipo":"pong","payload":"","ts":1710000181}
`
decoder := json.NewDecoder(strings.NewReader(ndjson))
for decoder.More() { // More() verifica se há mais valores
var e Evento
if err := decoder.Decode(&e); err != nil {
fmt.Println("Erro:", err)
return
}
fmt.Printf("tipo=%s ts=%d\n", e.Tipo, e.TS)
}
}
JSON dinâmico com map[string]any
Quando a estrutura do JSON não é conhecida em tempo de compilação, map[string]any recebe qualquer objeto JSON:
package main
import (
"encoding/json"
"fmt"
)
func main() {
jsonStr := `{
"nome": "Ricardo",
"idade": 35,
"ativo": true,
"scores": [9.5, 8.8, 9.1],
"endereco": {
"cidade": "São Paulo"
}
}`
var dados map[string]any
if err := json.Unmarshal([]byte(jsonStr), &dados); err != nil {
fmt.Println("Erro:", err)
return
}
// Acessando valores com type assertion
nome := dados["nome"].(string)
idade := dados["idade"].(float64) // JSON numbers → float64
ativo := dados["ativo"].(bool)
fmt.Printf("Nome: %s, Idade: %.0f, Ativo: %v\n", nome, idade, ativo)
// Slice de any
scores := dados["scores"].([]any)
for _, s := range scores {
fmt.Printf("%.1f ", s.(float64))
}
fmt.Println()
// Map aninhado
endereco := dados["endereco"].(map[string]any)
fmt.Println("Cidade:", endereco["cidade"])
}
Atenção: números em JSON sempre viram float64 quando deserializados em any. Para inteiros, use json.Decoder com UseNumber():
decoder := json.NewDecoder(strings.NewReader(jsonStr))
decoder.UseNumber() // preserva números como json.Number
var dados map[string]any
decoder.Decode(&dados)
idade, _ := dados["idade"].(json.Number).Int64()
fmt.Println(idade) // 35 como int64
json.RawMessage: adiando a deserialização
json.RawMessage armazena JSON bruto sem interpretá-lo, permitindo deserializar partes diferentes de um mesmo objeto em momentos diferentes:
package main
import (
"encoding/json"
"fmt"
)
type Mensagem struct {
Tipo string `json:"tipo"`
Payload json.RawMessage `json:"payload"` // serializado mas não interpretado
}
type PayloadLogin struct {
UserID int `json:"user_id"`
IP string `json:"ip"`
}
type PayloadCompra struct {
PedidoID int `json:"pedido_id"`
Valor float64 `json:"valor"`
}
func processarMensagem(dados []byte) error {
var msg Mensagem
if err := json.Unmarshal(dados, &msg); err != nil {
return err
}
switch msg.Tipo {
case "login":
var p PayloadLogin
json.Unmarshal(msg.Payload, &p)
fmt.Printf("Login: user=%d ip=%s\n", p.UserID, p.IP)
case "compra":
var p PayloadCompra
json.Unmarshal(msg.Payload, &p)
fmt.Printf("Compra: pedido=%d valor=R$%.2f\n", p.PedidoID, p.Valor)
default:
fmt.Printf("Tipo desconhecido: %s\n", msg.Tipo)
}
return nil
}
func main() {
mensagens := []string{
`{"tipo":"login","payload":{"user_id":42,"ip":"192.168.1.1"}}`,
`{"tipo":"compra","payload":{"pedido_id":1001,"valor":349.90}}`,
}
for _, m := range mensagens {
processarMensagem([]byte(m))
}
}
Customizando serialização com MarshalJSON e UnmarshalJSON
Para controle total sobre como um tipo é serializado, implemente as interfaces json.Marshaler e json.Unmarshaler:
package main
import (
"encoding/json"
"fmt"
"strings"
"time"
)
// Tipo de data com formato personalizado
type Data struct {
time.Time
}
func (d Data) MarshalJSON() ([]byte, error) {
return json.Marshal(d.Format("02/01/2006"))
}
func (d *Data) UnmarshalJSON(dados []byte) error {
var s string
if err := json.Unmarshal(dados, &s); err != nil {
return err
}
t, err := time.Parse("02/01/2006", s)
if err != nil {
return err
}
d.Time = t
return nil
}
// Tipo de status com representação textual
type Status int
const (
StatusPendente Status = iota
StatusAprovado
StatusRejeitado
)
func (s Status) MarshalJSON() ([]byte, error) {
nomes := map[Status]string{
StatusPendente: "pendente",
StatusAprovado: "aprovado",
StatusRejeitado: "rejeitado",
}
nome, ok := nomes[s]
if !ok {
return nil, fmt.Errorf("status desconhecido: %d", s)
}
return json.Marshal(nome)
}
func (s *Status) UnmarshalJSON(dados []byte) error {
var nome string
if err := json.Unmarshal(dados, &nome); err != nil {
return err
}
codigos := map[string]Status{
"pendente": StatusPendente,
"aprovado": StatusAprovado,
"rejeitado": StatusRejeitado,
}
codigo, ok := codigos[strings.ToLower(nome)]
if !ok {
return fmt.Errorf("status inválido: %s", nome)
}
*s = codigo
return nil
}
type Pedido struct {
ID int `json:"id"`
DataPedido Data `json:"data_pedido"`
Status Status `json:"status"`
Total float64 `json:"total"`
}
func main() {
pedido := Pedido{
ID: 1001,
DataPedido: Data{time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)},
Status: StatusAprovado,
Total: 549.90,
}
dados, _ := json.MarshalIndent(pedido, "", " ")
fmt.Println(string(dados))
// {
// "id": 1001,
// "data_pedido": "15/03/2024",
// "status": "aprovado",
// "total": 549.9
// }
jsonStr := `{"id":1002,"data_pedido":"20/03/2024","status":"pendente","total":299.90}`
var p2 Pedido
json.Unmarshal([]byte(jsonStr), &p2)
fmt.Printf("Pedido %d em %s — %v\n", p2.ID, p2.DataPedido.Format("02/01/2006"), p2.Status)
}
Validação de JSON
package main
import (
"encoding/json"
"fmt"
)
func validarJSON(dados []byte) bool {
return json.Valid(dados)
}
func main() {
valido := []byte(`{"nome":"Ricardo","idade":35}`)
invalido := []byte(`{"nome":"Ricardo","idade":}`)
fmt.Println(validarJSON(valido)) // true
fmt.Println(validarJSON(invalido)) // false
// Decodificação estrita — rejeita campos desconhecidos
jsonStr := `{"id":1,"nome":"Ana","campo_desconhecido":"valor"}`
decoder := json.NewDecoder(strings.NewReader(jsonStr))
decoder.DisallowUnknownFields()
var p Produto
if err := decoder.Decode(&p); err != nil {
fmt.Println("Erro:", err)
// json: unknown field "campo_desconhecido"
}
}
Resumo do que foi coberto
Este artigo apresentou JSON em Go de forma completa: Marshal e Unmarshal para conversões básicas, tags de struct com todas as opções disponíveis, MarshalIndent para saída formatada, Encoder e Decoder para streaming, map[string]any para JSON dinâmico com UseNumber, json.RawMessage para deserialização adiada, serialização customizada com MarshalJSON e UnmarshalJSON, e validação com json.Valid e DisallowUnknownFields. O próximo artigo constrói sobre esses fundamentos ao criar um servidor HTTP completo.
Referências e leituras complementares
-
Documentação do pacote encoding/json — Referência completa de todas as funções e tipos. https://pkg.go.dev/encoding/json
-
Go Blog: JSON and Go — Artigo introdutório oficial sobre JSON em Go. https://go.dev/blog/json
-
Go by Example: JSON — Exemplos práticos comentados. https://gobyexample.com/json
-
json-iterator/go — Implementação alternativa compatível com encoding/json, mais performática. https://github.com/json-iterator/go
-
go-playground/validator — Biblioteca popular para validação de structs com tags. https://github.com/go-playground/validator
-
tidwall/gjson — Biblioteca para consultas rápidas em JSON sem deserialização completa. https://github.com/tidwall/gjson
Próximo artigo: Artigo 28 — Criando um servidor HTTP com net/http**
you asked
Sim