Go Admin

Pacote encoding/json em Go: Serialização, Tags e Casos Especiais: Do Básico ao Avançado Já leu

Introdução ao Pacote encoding/json em Go O pacote é uma das ferramentas mais fundamentais em Go quando o assunto é trabalhar com dados estruturados. JSON (JavaScript Object Notation) tornou-se o padrão de facto para troca de dados em APIs REST, e Go oferece suporte nativo e extremamente eficiente para serialização e desserialização. A principal função deste pacote é converter estruturas Go em JSON (marshal) e vice-versa (unmarshal), permitindo que dados sejam transmitidos pela rede ou persistidos em arquivos de forma legível e interoperável. O que torna o especialmente poderoso em Go é sua integração com reflection e tags de estrutura. Isso significa que você não precisa escrever código boilerplate como em outras linguagens — simplesmente defina suas estruturas de forma adequada e o Go cuida do resto. Nesta aula, exploraremos não apenas o básico, mas também os padrões avançados que diferenciam um desenvolvedor profissional de alguém que apenas copia e cola código. Marshal: Convertendo Estruturas Go para JSON Conceito Fundamental

Introdução ao Pacote encoding/json em Go

O pacote encoding/json é uma das ferramentas mais fundamentais em Go quando o assunto é trabalhar com dados estruturados. JSON (JavaScript Object Notation) tornou-se o padrão de facto para troca de dados em APIs REST, e Go oferece suporte nativo e extremamente eficiente para serialização e desserialização. A principal função deste pacote é converter estruturas Go em JSON (marshal) e vice-versa (unmarshal), permitindo que dados sejam transmitidos pela rede ou persistidos em arquivos de forma legível e interoperável.

O que torna o encoding/json especialmente poderoso em Go é sua integração com reflection e tags de estrutura. Isso significa que você não precisa escrever código boilerplate como em outras linguagens — simplesmente defina suas estruturas de forma adequada e o Go cuida do resto. Nesta aula, exploraremos não apenas o básico, mas também os padrões avançados que diferenciam um desenvolvedor profissional de alguém que apenas copia e cola código.

Marshal: Convertendo Estruturas Go para JSON

Conceito Fundamental

Marshal é o processo de transformar um objeto Go em sua representação JSON. Quando você chama json.Marshal(), o Go inspecciona a estrutura usando reflection, lê seus campos públicos (aqueles que começam com letra maiúscula) e os serializa. O resultado é um slice de bytes contendo uma representação JSON compacta. Para uma versão formatada e legível, existe json.MarshalIndent().

Vamos começar com um exemplo prático:

package main

import (
    "encoding/json"
    "fmt"
    "log"
)

type Pessoa struct {
    Nome      string
    Idade     int
    Email     string
    Ativo     bool
}

func main() {
    p := Pessoa{
        Nome:  "Alice Silva",
        Idade: 28,
        Email: "alice@example.com",
        Ativo: true,
    }

    // Marshal compacto
    jsonBytes, err := json.Marshal(p)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Compacto:", string(jsonBytes))

    // Marshal com indentação
    jsonFormatado, err := json.MarshalIndent(p, "", "  ")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("\nFormatado:")
    fmt.Println(string(jsonFormatado))
}

Quando você executa este código, o resultado será:

Compacto: {"Nome":"Alice Silva","Idade":28,"Email":"alice@example.com","Ativo":true}

Formatado:
{
  "Nome": "Alice Silva",
  "Idade": 28,
  "Email": "alice@example.com",
  "Ativo": true
}

Note que os nomes dos campos na saída JSON começam com letra maiúscula, refletindo os nomes das variáveis Go. Isso é considerado uma má prática em APIs profissionais, pois JSON tipicamente usa camelCase ou snake_case. É aqui que as tags entram em cena, como veremos na próxima seção.

Tratamento de Erros em Marshal

Marshal pode falhar em situações específicas. O caso mais comum é tentar serializar valores que não são JSON-serializáveis, como canais ou funções. Go também rejeita valores circulares automaticamente para evitar loops infinitos. Sempre verifique o erro retornado:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "math"
)

type Config struct {
    Timeout   int
    MaxValue  float64
}

func main() {
    // Caso 1: Valor inválido (infinito)
    config := Config{
        Timeout:  30,
        MaxValue: math.Inf(1),
    }

    _, err := json.Marshal(config)
    if err != nil {
        fmt.Println("Erro:", err)
        // Output: Erro: json: unsupported value: +Inf
    }

    // Caso 2: Marshal bem-sucedido com valores válidos
    config2 := Config{
        Timeout:  30,
        MaxValue: 99.99,
    }

    result, _ := json.Marshal(config2)
    fmt.Println(string(result))
    // Output: {"Timeout":30,"MaxValue":99.99}
}

Tags de Estrutura: Controlando a Serialização

O Sistema de Tags em Go

Tags são metadados anexados aos campos de uma estrutura que instruem o Go como processar cada campo. Em encoding/json, a tag padrão é json:"nome_no_json". Essa sintaxe simples é incrivelmente poderosa quando combinada com opcões. As tags não afetam o comportamento em tempo de execução do Go — elas existem exclusivamente para reflexão.

Vamos explorar os diferentes tipos de tags e suas aplicações:

package main

import (
    "encoding/json"
    "fmt"
)

type Usuario struct {
    // Nome usual: campo será serializado como "id"
    ID int `json:"id"`

    // Snake case: padrão comum em APIs REST
    NomeCompleto string `json:"nome_completo"`

    // Omitempty: não inclui o campo se estiver vazio
    Biografia string `json:"biografia,omitempty"`

    // Dash: ignora completamente o campo
    SenhaHash string `json:"-"`

    // Campo privado: ignorado automaticamente
    tokenInterno string

    // String tag: força conversão para string no JSON
    Score int `json:"score,string"`

    // Múltiplas opções
    Ativo bool `json:"ativo,omitempty"`
}

func main() {
    u := Usuario{
        ID:           1,
        NomeCompleto: "João da Silva",
        Biografia:    "",
        SenhaHash:    "super_secret_hash",
        Score:        9500,
        Ativo:        true,
    }

    jsonBytes, _ := json.MarshalIndent(u, "", "  ")
    fmt.Println(string(jsonBytes))
}

O output será:

{
  "id": 1,
  "nome_completo": "João da Silva",
  "score": "9500",
  "ativo": true
}

Observe que Biografia desapareceu porque estava vazia e tinha omitempty, SenhaHash foi completamente ignorada, Score virou string como especificado, e o campo privado tokenInterno foi ignorado automaticamente.

Tags Avançadas e Casos de Uso

As tags podem ser combinadas para criar comportamentos sofisticados. O opção string é particularmente útil quando você precisa que números sejam representados como strings no JSON, um requisito comum em APIs financeiras onde a precisão é crítica.

package main

import (
    "encoding/json"
    "fmt"
)

type Produto struct {
    ID    int64  `json:"id,string"`
    Nome  string `json:"nome"`
    Preco float64 `json:"preco,string"`
    // Você pode usar string com qualquer tipo numérico
    Estoque int `json:"estoque,string"`
    // Opções podem ser combinadas
    Descricao string `json:"descricao,omitempty"`
    // Ignore este campo completamente
    CustoInterno float64 `json:"-"`
}

func main() {
    p := Produto{
        ID:           987654321,
        Nome:         "Notebook",
        Preco:        4500.99,
        Estoque:      15,
        Descricao:    "",
        CustoInterno: 2000.00,
    }

    json, _ := json.MarshalIndent(p, "", "  ")
    fmt.Println(string(json))
}

Output:

{
  "id": "987654321",
  "nome": "Notebook",
  "preco": "4500.99",
  "estoque": "15"
}

Unmarshal: Desserializando JSON para Estruturas Go

Fundamentos da Desserialização

Unmarshal é o inverso de marshal — você fornece um slice de bytes contendo JSON e uma estrutura Go como destino, e o Go preencherá essa estrutura com os dados. O tipo de dado deve ser um ponteiro, pois unmarshal precisa modificar a estrutura. Isso é absolutamente fundamental: se você esquecer do ponteiro, seu código compila mas a estrutura permanece vazia.

package main

import (
    "encoding/json"
    "fmt"
    "log"
)

type Livro struct {
    Titulo  string `json:"titulo"`
    Autor   string `json:"autor"`
    Ano     int    `json:"ano"`
    Paginas int    `json:"paginas,omitempty"`
}

func main() {
    jsonData := []byte(`{
        "titulo": "Clean Code",
        "autor": "Robert Martin",
        "ano": 2008,
        "paginas": 464
    }`)

    var livro Livro
    err := json.Unmarshal(jsonData, &livro)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Livro: %+v\n", livro)
    // Output: Livro: {Titulo:Clean Code Autor:Robert Martin Ano:2008 Paginas:464}
}

Um ponto importante: Go é tolerante com campos extras no JSON. Se o JSON contiver campos que não existem na estrutura Go, eles são simplesmente ignorados. Isso é uma decisão de design que facilita trabalhar com APIs que evoluem e adicionam novos campos.

Tratamento de Erros em Unmarshal

O unmarshal pode falhar de várias formas: JSON inválido, tipos incompatíveis, ou estrutura inesperada. A forma mais comum é quando os dados no JSON não correspondem ao tipo esperado na estrutura Go.

package main

import (
    "encoding/json"
    "fmt"
    "log"
)

type Configuracao struct {
    Timeout int    `json:"timeout"`
    Host    string `json:"host"`
    Porta   int    `json:"porta"`
}

func main() {
    // Caso 1: JSON inválido
    jsonInvalido := []byte(`{"timeout": 30, "host": "localhost"`)
    var config Configuracao
    err := json.Unmarshal(jsonInvalido, &config)
    fmt.Println("Erro JSON inválido:", err)
    // unexpected end of JSON input

    // Caso 2: Tipo incompatível
    jsonTipoErrado := []byte(`{"timeout": "trinta", "host": "localhost", "porta": 8080}`)
    err = json.Unmarshal(jsonTipoErrado, &config)
    fmt.Println("Erro tipo incompatível:", err)
    // json: cannot unmarshal string into Go struct field Configuracao.timeout of type int

    // Caso 3: Sucesso
    jsonValido := []byte(`{"timeout": 30, "host": "localhost", "porta": 8080}`)
    err = json.Unmarshal(jsonValido, &config)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Sucesso: %+v\n", config)
    // Sucesso: {Timeout:30 Host:localhost Porta:8080}
}

Tipos Dinâmicos com interface{}

Às vezes, você não conhece a estrutura do JSON antecipadamente. Nessas situações, use interface{} para capturar qualquer tipo. O resultado será um map ou slice que você pode inspecionar em tempo de execução.

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    jsonData := []byte(`{
        "nome": "Maria",
        "idade": 25,
        "ativo": true,
        "hobbies": ["leitura", "programação"]
    }`)

    var dados interface{}
    json.Unmarshal(jsonData, &dados)

    // dados agora é um map[string]interface{}
    m := dados.(map[string]interface{})

    fmt.Println("Nome:", m["nome"])
    fmt.Println("Idade:", m["idade"])
    fmt.Println("Ativo:", m["ativo"])

    hobbies := m["hobbies"].([]interface{})
    for i, hobby := range hobbies {
        fmt.Printf("Hobby %d: %v\n", i+1, hobby)
    }
}

Casos Especiais e Padrões Avançados

Valores Nulos e Ponteiros

Em JSON, null é um valor completamente diferente de um campo ausente. Para representar isso em Go, use ponteiros. Um campo de ponteiro que é nil será serializado como null, e ao desserializar, um null no JSON resultará em nil no Go.

package main

import (
    "encoding/json"
    "fmt"
)

type Perfil struct {
    Nome      string `json:"nome"`
    Sobrenome *string `json:"sobrenome"`
    Telefone  *string `json:"telefone,omitempty"`
    Idade     *int    `json:"idade"`
}

func main() {
    // Serialização: null values
    idade := 30
    p1 := Perfil{
        Nome:      "Carlos",
        Sobrenome: nil,
        Telefone:  nil,
        Idade:     &idade,
    }

    json1, _ := json.MarshalIndent(p1, "", "  ")
    fmt.Println("Com nil:")
    fmt.Println(string(json1))

    // Desserialização: null values
    jsonData := []byte(`{
        "nome": "Ana",
        "sobrenome": null,
        "idade": 28
    }`)

    var p2 Perfil
    json.Unmarshal(jsonData, &p2)
    fmt.Printf("\nDesserializado: %+v\n", p2)
    fmt.Println("Sobrenome é nil?", p2.Sobrenome == nil)
}

Output:

Com nil:
{
  "nome": "Carlos",
  "sobrenome": null,
  "idade": 30
}

Desserializado: {Nome:Ana Sobrenome:<nil> Telefone:<nil> Idade:0x...}
Sobrenome é nil? true

Tipos Customizados e Marshaler Interface

Para tipos customizados ou quando você precisa de lógica especial de serialização, implemente as interfaces json.Marshaler e json.Unmarshaler. Isso permite controle total sobre como seu tipo é representado em JSON.

package main

import (
    "encoding/json"
    "fmt"
    "time"
)

type Data struct {
    Tempo time.Time
}

// Implementar json.Marshaler
func (d Data) MarshalJSON() ([]byte, error) {
    return json.Marshal(d.Tempo.Format("02/01/2006"))
}

// Implementar json.Unmarshaler
func (d *Data) UnmarshalJSON(b []byte) error {
    var s string
    err := json.Unmarshal(b, &s)
    if err != nil {
        return err
    }
    t, err := time.Parse("02/01/2006", s)
    if err != nil {
        return err
    }
    d.Tempo = t
    return nil
}

type Evento struct {
    Nome string `json:"nome"`
    Data Data   `json:"data"`
}

func main() {
    // Marshal
    t, _ := time.Parse("2006-01-02", "2024-12-25")
    evento := Evento{
        Nome: "Natal",
        Data: Data{Tempo: t},
    }

    jsonBytes, _ := json.MarshalIndent(evento, "", "  ")
    fmt.Println("Marshal:")
    fmt.Println(string(jsonBytes))

    // Unmarshal
    jsonData := []byte(`{"nome":"Ano Novo","data":"01/01/2025"}`)
    var evento2 Evento
    json.Unmarshal(jsonData, &evento2)
    fmt.Printf("\nUnmarshal: %+v\n", evento2)
}

Output:

Marshal:
{
  "nome": "Natal",
  "data": "25/12/2024"
}

Unmarshal: {Nome:Ano Novo Data:{Tempo:2025-01-01 00:00:00 +0000 UTC}}

Slices, Maps e Estruturas Aninhadas

JSON suporta arrays e objetos aninhados de forma nativa, e Go mapeia isso perfeitamente para slices e maps. Estruturas aninhadas funcionam exatamente como esperado quando você segue as convenções de tags.

package main

import (
    "encoding/json"
    "fmt"
)

type Endereco struct {
    Rua    string `json:"rua"`
    Cidade string `json:"cidade"`
    CEP    string `json:"cep"`
}

type Contato struct {
    Tipo  string `json:"tipo"`
    Valor string `json:"valor"`
}

type Cliente struct {
    ID        int                `json:"id"`
    Nome      string             `json:"nome"`
    Endereco  Endereco           `json:"endereco"`
    Contatos  []Contato          `json:"contatos"`
    Metadata  map[string]string  `json:"metadata,omitempty"`
}

func main() {
    cliente := Cliente{
        ID:   123,
        Nome: "Empresa XYZ",
        Endereco: Endereco{
            Rua:    "Av. Paulista, 1000",
            Cidade: "São Paulo",
            CEP:    "01311-100",
        },
        Contatos: []Contato{
            {Tipo: "email", Valor: "contato@xyz.com"},
            {Tipo: "telefone", Valor: "+55 11 3000-0000"},
        },
        Metadata: map[string]string{
            "origem":       "formulário web",
            "prioridade":   "alta",
        },
    }

    json, _ := json.MarshalIndent(cliente, "", "  ")
    fmt.Println(string(json))
}

Output:

{
  "id": 123,
  "nome": "Empresa XYZ",
  "endereco": {
    "rua": "Av. Paulista, 1000",
    "cidade": "São Paulo",
    "cep": "01311-100"
  },
  "contatos": [
    {
      "tipo": "email",
      "valor": "contato@xyz.com"
    },
    {
      "tipo": "telefone",
      "valor": "+55 11 3000-0000"
    }
  ],
  "metadata": {
    "origem": "formulário web",
    "prioridade": "alta"
  }
}

Conclusão

Os três pilares do domínio do encoding/json em Go são: (1) Marshal e Unmarshal são operações simétricas que você executará constantemente — domine o fluxo de conversão e o tratamento de erros; (2) Tags de estrutura são o mecanismo que transforma Go em uma máquina de serialização profissional — use omitempty, string, e nomes customizados de forma estratégica; (3) Casos especiais como ponteiros para null, tipos customizados com Marshaler interface, e estruturas aninhadas existem para resolver problemas reais — não são apenas recursos esotéricos, mas sim padrões que você encontrará em produção. A combinação desses conhecimentos o capacita a construir APIs robustas e interoperar com sistemas de forma confiável.

Referências


Artigos relacionados