Go

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

19 min de leitura

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

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

<h2>Introdução ao Pacote encoding/json em Go</h2>

<p>O pacote <code>encoding/json</code> é 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.</p>

<p>O que torna o <code>encoding/json</code> 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.</p>

<h2>Marshal: Convertendo Estruturas Go para JSON</h2>

<h3>Conceito Fundamental</h3>

<p>Marshal é o processo de transformar um objeto Go em sua representação JSON. Quando você chama <code>json.Marshal()</code>, 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 <code>json.MarshalIndent()</code>.</p>

<p>Vamos começar com um exemplo prático:</p>

<pre><code class="language-go">package main

import (

&quot;encoding/json&quot;

&quot;fmt&quot;

&quot;log&quot;

)

type Pessoa struct {

Nome string

Idade int

Email string

Ativo bool

}

func main() {

p := Pessoa{

Nome: &quot;Alice Silva&quot;,

Idade: 28,

Email: &quot;alice@example.com&quot;,

Ativo: true,

}

// Marshal compacto

jsonBytes, err := json.Marshal(p)

if err != nil {

log.Fatal(err)

}

fmt.Println(&quot;Compacto:&quot;, string(jsonBytes))

// Marshal com indentação

jsonFormatado, err := json.MarshalIndent(p, &quot;&quot;, &quot; &quot;)

if err != nil {

log.Fatal(err)

}

fmt.Println(&quot;\nFormatado:&quot;)

fmt.Println(string(jsonFormatado))

}</code></pre>

<p>Quando você executa este código, o resultado será:</p>

<pre><code>Compacto: {&quot;Nome&quot;:&quot;Alice Silva&quot;,&quot;Idade&quot;:28,&quot;Email&quot;:&quot;alice@example.com&quot;,&quot;Ativo&quot;:true}

Formatado:

{

&quot;Nome&quot;: &quot;Alice Silva&quot;,

&quot;Idade&quot;: 28,

&quot;Email&quot;: &quot;alice@example.com&quot;,

&quot;Ativo&quot;: true

}</code></pre>

<p>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.</p>

<h3>Tratamento de Erros em Marshal</h3>

<p>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:</p>

<pre><code class="language-go">package main

import (

&quot;encoding/json&quot;

&quot;fmt&quot;

&quot;log&quot;

&quot;math&quot;

)

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(&quot;Erro:&quot;, 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: {&quot;Timeout&quot;:30,&quot;MaxValue&quot;:99.99}

}</code></pre>

<h2>Tags de Estrutura: Controlando a Serialização</h2>

<h3>O Sistema de Tags em Go</h3>

<p>Tags são metadados anexados aos campos de uma estrutura que instruem o Go como processar cada campo. Em <code>encoding/json</code>, a tag padrão é <code>json:&quot;nome_no_json&quot;</code>. 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.</p>

<p>Vamos explorar os diferentes tipos de tags e suas aplicações:</p>

<pre><code class="language-go">package main

import (

&quot;encoding/json&quot;

&quot;fmt&quot;

)

type Usuario struct {

// Nome usual: campo será serializado como &quot;id&quot;

ID int json:&quot;id&quot;

// Snake case: padrão comum em APIs REST

NomeCompleto string json:&quot;nome_completo&quot;

// Omitempty: não inclui o campo se estiver vazio

Biografia string json:&quot;biografia,omitempty&quot;

// Dash: ignora completamente o campo

SenhaHash string json:&quot;-&quot;

// Campo privado: ignorado automaticamente

tokenInterno string

// String tag: força conversão para string no JSON

Score int json:&quot;score,string&quot;

// Múltiplas opções

Ativo bool json:&quot;ativo,omitempty&quot;

}

func main() {

u := Usuario{

ID: 1,

NomeCompleto: &quot;João da Silva&quot;,

Biografia: &quot;&quot;,

SenhaHash: &quot;super_secret_hash&quot;,

Score: 9500,

Ativo: true,

}

jsonBytes, _ := json.MarshalIndent(u, &quot;&quot;, &quot; &quot;)

fmt.Println(string(jsonBytes))

}</code></pre>

<p>O output será:</p>

<pre><code class="language-json">{

&quot;id&quot;: 1,

&quot;nome_completo&quot;: &quot;João da Silva&quot;,

&quot;score&quot;: &quot;9500&quot;,

&quot;ativo&quot;: true

}</code></pre>

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

<h3>Tags Avançadas e Casos de Uso</h3>

<p>As tags podem ser combinadas para criar comportamentos sofisticados. O opção <code>string</code> é 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.</p>

<pre><code class="language-go">package main

import (

&quot;encoding/json&quot;

&quot;fmt&quot;

)

type Produto struct {

ID int64 json:&quot;id,string&quot;

Nome string json:&quot;nome&quot;

Preco float64 json:&quot;preco,string&quot;

// Você pode usar string com qualquer tipo numérico

Estoque int json:&quot;estoque,string&quot;

// Opções podem ser combinadas

Descricao string json:&quot;descricao,omitempty&quot;

// Ignore este campo completamente

CustoInterno float64 json:&quot;-&quot;

}

func main() {

p := Produto{

ID: 987654321,

Nome: &quot;Notebook&quot;,

Preco: 4500.99,

Estoque: 15,

Descricao: &quot;&quot;,

CustoInterno: 2000.00,

}

json, _ := json.MarshalIndent(p, &quot;&quot;, &quot; &quot;)

fmt.Println(string(json))

}</code></pre>

<p>Output:</p>

<pre><code class="language-json">{

&quot;id&quot;: &quot;987654321&quot;,

&quot;nome&quot;: &quot;Notebook&quot;,

&quot;preco&quot;: &quot;4500.99&quot;,

&quot;estoque&quot;: &quot;15&quot;

}</code></pre>

<h2>Unmarshal: Desserializando JSON para Estruturas Go</h2>

<h3>Fundamentos da Desserialização</h3>

<p>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.</p>

<pre><code class="language-go">package main

import (

&quot;encoding/json&quot;

&quot;fmt&quot;

&quot;log&quot;

)

type Livro struct {

Titulo string json:&quot;titulo&quot;

Autor string json:&quot;autor&quot;

Ano int json:&quot;ano&quot;

Paginas int json:&quot;paginas,omitempty&quot;

}

func main() {

jsonData := []byte(`{

&quot;titulo&quot;: &quot;Clean Code&quot;,

&quot;autor&quot;: &quot;Robert Martin&quot;,

&quot;ano&quot;: 2008,

&quot;paginas&quot;: 464

}`)

var livro Livro

err := json.Unmarshal(jsonData, &amp;livro)

if err != nil {

log.Fatal(err)

}

fmt.Printf(&quot;Livro: %+v\n&quot;, livro)

// Output: Livro: {Titulo:Clean Code Autor:Robert Martin Ano:2008 Paginas:464}

}</code></pre>

<p>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.</p>

<h3>Tratamento de Erros em Unmarshal</h3>

<p>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.</p>

<pre><code class="language-go">package main

import (

&quot;encoding/json&quot;

&quot;fmt&quot;

&quot;log&quot;

)

type Configuracao struct {

Timeout int json:&quot;timeout&quot;

Host string json:&quot;host&quot;

Porta int json:&quot;porta&quot;

}

func main() {

// Caso 1: JSON inválido

jsonInvalido := []byte({&quot;timeout&quot;: 30, &quot;host&quot;: &quot;localhost&quot;)

var config Configuracao

err := json.Unmarshal(jsonInvalido, &amp;config)

fmt.Println(&quot;Erro JSON inválido:&quot;, err)

// unexpected end of JSON input

// Caso 2: Tipo incompatível

jsonTipoErrado := []byte({&quot;timeout&quot;: &quot;trinta&quot;, &quot;host&quot;: &quot;localhost&quot;, &quot;porta&quot;: 8080})

err = json.Unmarshal(jsonTipoErrado, &amp;config)

fmt.Println(&quot;Erro tipo incompatível:&quot;, err)

// json: cannot unmarshal string into Go struct field Configuracao.timeout of type int

// Caso 3: Sucesso

jsonValido := []byte({&quot;timeout&quot;: 30, &quot;host&quot;: &quot;localhost&quot;, &quot;porta&quot;: 8080})

err = json.Unmarshal(jsonValido, &amp;config)

if err != nil {

log.Fatal(err)

}

fmt.Printf(&quot;Sucesso: %+v\n&quot;, config)

// Sucesso: {Timeout:30 Host:localhost Porta:8080}

}</code></pre>

<h3>Tipos Dinâmicos com interface{}</h3>

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

<pre><code class="language-go">package main

import (

&quot;encoding/json&quot;

&quot;fmt&quot;

)

func main() {

jsonData := []byte(`{

&quot;nome&quot;: &quot;Maria&quot;,

&quot;idade&quot;: 25,

&quot;ativo&quot;: true,

&quot;hobbies&quot;: [&quot;leitura&quot;, &quot;programação&quot;]

}`)

var dados interface{}

json.Unmarshal(jsonData, &amp;dados)

// dados agora é um map[string]interface{}

m := dados.(map[string]interface{})

fmt.Println(&quot;Nome:&quot;, m[&quot;nome&quot;])

fmt.Println(&quot;Idade:&quot;, m[&quot;idade&quot;])

fmt.Println(&quot;Ativo:&quot;, m[&quot;ativo&quot;])

hobbies := m[&quot;hobbies&quot;].([]interface{})

for i, hobby := range hobbies {

fmt.Printf(&quot;Hobby %d: %v\n&quot;, i+1, hobby)

}

}</code></pre>

<h2>Casos Especiais e Padrões Avançados</h2>

<h3>Valores Nulos e Ponteiros</h3>

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

<pre><code class="language-go">package main

import (

&quot;encoding/json&quot;

&quot;fmt&quot;

)

type Perfil struct {

Nome string json:&quot;nome&quot;

Sobrenome *string json:&quot;sobrenome&quot;

Telefone *string json:&quot;telefone,omitempty&quot;

Idade *int json:&quot;idade&quot;

}

func main() {

// Serialização: null values

idade := 30

p1 := Perfil{

Nome: &quot;Carlos&quot;,

Sobrenome: nil,

Telefone: nil,

Idade: &amp;idade,

}

json1, _ := json.MarshalIndent(p1, &quot;&quot;, &quot; &quot;)

fmt.Println(&quot;Com nil:&quot;)

fmt.Println(string(json1))

// Desserialização: null values

jsonData := []byte(`{

&quot;nome&quot;: &quot;Ana&quot;,

&quot;sobrenome&quot;: null,

&quot;idade&quot;: 28

}`)

var p2 Perfil

json.Unmarshal(jsonData, &amp;p2)

fmt.Printf(&quot;\nDesserializado: %+v\n&quot;, p2)

fmt.Println(&quot;Sobrenome é nil?&quot;, p2.Sobrenome == nil)

}</code></pre>

<p>Output:</p>

<pre><code>Com nil:

{

&quot;nome&quot;: &quot;Carlos&quot;,

&quot;sobrenome&quot;: null,

&quot;idade&quot;: 30

}

Desserializado: {Nome:Ana Sobrenome:&lt;nil&gt; Telefone:&lt;nil&gt; Idade:0x...}

Sobrenome é nil? true</code></pre>

<h3>Tipos Customizados e Marshaler Interface</h3>

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

<pre><code class="language-go">package main

import (

&quot;encoding/json&quot;

&quot;fmt&quot;

&quot;time&quot;

)

type Data struct {

Tempo time.Time

}

// Implementar json.Marshaler

func (d Data) MarshalJSON() ([]byte, error) {

return json.Marshal(d.Tempo.Format(&quot;02/01/2006&quot;))

}

// Implementar json.Unmarshaler

func (d *Data) UnmarshalJSON(b []byte) error {

var s string

err := json.Unmarshal(b, &amp;s)

if err != nil {

return err

}

t, err := time.Parse(&quot;02/01/2006&quot;, s)

if err != nil {

return err

}

d.Tempo = t

return nil

}

type Evento struct {

Nome string json:&quot;nome&quot;

Data Data json:&quot;data&quot;

}

func main() {

// Marshal

t, _ := time.Parse(&quot;2006-01-02&quot;, &quot;2024-12-25&quot;)

evento := Evento{

Nome: &quot;Natal&quot;,

Data: Data{Tempo: t},

}

jsonBytes, _ := json.MarshalIndent(evento, &quot;&quot;, &quot; &quot;)

fmt.Println(&quot;Marshal:&quot;)

fmt.Println(string(jsonBytes))

// Unmarshal

jsonData := []byte({&quot;nome&quot;:&quot;Ano Novo&quot;,&quot;data&quot;:&quot;01/01/2025&quot;})

var evento2 Evento

json.Unmarshal(jsonData, &amp;evento2)

fmt.Printf(&quot;\nUnmarshal: %+v\n&quot;, evento2)

}</code></pre>

<p>Output:</p>

<pre><code>Marshal:

{

&quot;nome&quot;: &quot;Natal&quot;,

&quot;data&quot;: &quot;25/12/2024&quot;

}

Unmarshal: {Nome:Ano Novo Data:{Tempo:2025-01-01 00:00:00 +0000 UTC}}</code></pre>

<h3>Slices, Maps e Estruturas Aninhadas</h3>

<p>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.</p>

<pre><code class="language-go">package main

import (

&quot;encoding/json&quot;

&quot;fmt&quot;

)

type Endereco struct {

Rua string json:&quot;rua&quot;

Cidade string json:&quot;cidade&quot;

CEP string json:&quot;cep&quot;

}

type Contato struct {

Tipo string json:&quot;tipo&quot;

Valor string json:&quot;valor&quot;

}

type Cliente struct {

ID int json:&quot;id&quot;

Nome string json:&quot;nome&quot;

Endereco Endereco json:&quot;endereco&quot;

Contatos []Contato json:&quot;contatos&quot;

Metadata map[string]string json:&quot;metadata,omitempty&quot;

}

func main() {

cliente := Cliente{

ID: 123,

Nome: &quot;Empresa XYZ&quot;,

Endereco: Endereco{

Rua: &quot;Av. Paulista, 1000&quot;,

Cidade: &quot;São Paulo&quot;,

CEP: &quot;01311-100&quot;,

},

Contatos: []Contato{

{Tipo: &quot;email&quot;, Valor: &quot;contato@xyz.com&quot;},

{Tipo: &quot;telefone&quot;, Valor: &quot;+55 11 3000-0000&quot;},

},

Metadata: map[string]string{

&quot;origem&quot;: &quot;formulário web&quot;,

&quot;prioridade&quot;: &quot;alta&quot;,

},

}

json, _ := json.MarshalIndent(cliente, &quot;&quot;, &quot; &quot;)

fmt.Println(string(json))

}</code></pre>

<p>Output:</p>

<pre><code class="language-json">{

&quot;id&quot;: 123,

&quot;nome&quot;: &quot;Empresa XYZ&quot;,

&quot;endereco&quot;: {

&quot;rua&quot;: &quot;Av. Paulista, 1000&quot;,

&quot;cidade&quot;: &quot;São Paulo&quot;,

&quot;cep&quot;: &quot;01311-100&quot;

},

&quot;contatos&quot;: [

{

&quot;tipo&quot;: &quot;email&quot;,

&quot;valor&quot;: &quot;contato@xyz.com&quot;

},

{

&quot;tipo&quot;: &quot;telefone&quot;,

&quot;valor&quot;: &quot;+55 11 3000-0000&quot;

}

],

&quot;metadata&quot;: {

&quot;origem&quot;: &quot;formulário web&quot;,

&quot;prioridade&quot;: &quot;alta&quot;

}

}</code></pre>

<h2>Conclusão</h2>

<p>Os três pilares do domínio do <code>encoding/json</code> em Go são: (1) <strong>Marshal e Unmarshal</strong> são operações simétricas que você executará constantemente — domine o fluxo de conversão e o tratamento de erros; (2) <strong>Tags de estrutura</strong> são o mecanismo que transforma Go em uma máquina de serialização profissional — use <code>omitempty</code>, <code>string</code>, e nomes customizados de forma estratégica; (3) <strong>Casos especiais</strong> 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.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://golang.org/pkg/encoding/json/" target="_blank" rel="noopener noreferrer">Official Go Documentation - encoding/json</a></li>

<li><a href="https://golang.org/doc/effective_go#json" target="_blank" rel="noopener noreferrer">Effective Go - JSON and encoding</a></li>

<li><a href="https://golang.org/blog/json" target="_blank" rel="noopener noreferrer">The Go Blog - JSON and Go</a></li>

<li><a href="https://dave.cheney.net/practical-go" target="_blank" rel="noopener noreferrer">Dave Cheney - Practical Go: Real-world advice for writing maintainable Go programs</a></li>

<li><a href="https://www.jetbrains.com/help/go/json.html" target="_blank" rel="noopener noreferrer">GoLand IDE - Working with JSON in Go</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Go

Introdução ao Go: Filosofia, Instalação, Toolchain e Primeiro Programa na Prática
Introdução ao Go: Filosofia, Instalação, Toolchain e Primeiro Programa na Prática

Por que Go? A Filosofia por Trás da Linguagem Go (ou Golang) foi criada em 20...

O que Todo Dev Deve Saber sobre SQLC em Go: Gerando Código Tipado a partir de Queries SQL
O que Todo Dev Deve Saber sobre SQLC em Go: Gerando Código Tipado a partir de Queries SQL

O que é SQLC e Por que Você Deveria Usar SQLC é uma ferramenta que gera códig...

O que Todo Dev Deve Saber sobre Garbage Collector em Go: Funcionamento Interno e Impacto na Performance
O que Todo Dev Deve Saber sobre Garbage Collector em Go: Funcionamento Interno e Impacto na Performance

Introdução: O que é o Garbage Collector em Go? O Garbage Collector (GC) é um...