Golang

Artigo 27 — Trabalhando com JSON: encode, decode e tags de struct Já leu

11 min de leitura

Artigo 27 — Trabalhando com JSON: encode, decode e tags de struct
Artigo 27 — Trabalhando com JSON: encode, decode e tags de struct JSON: a língua franca das APIs modernas JSON tornou-se o formato padrão de troca de dados

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ção
  • string — 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


claude response

Comentários

Mais em Golang

A biblioteca padrão: um tour pelas principais ferramentas
A biblioteca padrão: um tour pelas principais ferramentas

Uma das razões pelas quais Go se tornou a linguagem preferida para inf...

Testes em Go: testing, tabelas de casos e benchmarks
Testes em Go: testing, tabelas de casos e benchmarks

Em muitas linguagens, testar código exige escolher entre uma dúzia de framewo...

Artigo 50 — Go na nuvem: deploy no Railway, Fly.io e Google Cloud Run
Artigo 50 — Go na nuvem: deploy no Railway, Fly.io e Google Cloud Run

Artigo 50 — Go na nuvem: deploy no Railway, Fly.io e Google Cloud Run Três pl...