O que é JSON e como o Go trabalha com isso
JSON (JavaScript Object Notation) é um formato de texto para representar dados estruturados (objetos, listas, números, strings, booleanos e null). Em Go, o pacote encoding/json faz a ponte entre dados Go (principalmente struct, map e slice) e JSON, através de dois processos:
- Marshaling: converter um valor Go em JSON (
json.Marshal/json.Encoder). - Unmarshaling: converter JSON em um valor Go (
json.Unmarshal/json.Decoder).
O mapeamento é guiado por regras do pacote e por tags no struct (ex.: json:"name"), o que permite controlar nomes de campos, omissões e compatibilidade com APIs.
Marshaling: de struct para JSON
Exemplo básico com tags
Campos exportados (iniciando com letra maiúscula) são serializados. Tags definem o nome no JSON e opções como omitempty.
package main
import (
"encoding/json"
"fmt"
"time"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
func main() {
u := User{
ID: 10,
Name: "Ana",
Email: "", // será omitido por omitempty
CreatedAt: time.Date(2026, 1, 25, 10, 0, 0, 0, time.UTC),
}
b, err := json.Marshal(u)
if err != nil {
panic(err)
}
fmt.Println(string(b))
}
Observações importantes:
time.Timeé serializado por padrão como string no formato RFC3339 (ex.:"2026-01-25T10:00:00Z").omitemptyomite o campo se ele estiver no “zero value” do tipo (string vazia, número 0,false, ponteiro nil, slice/map nil,time.Time{}etc.).
JSON formatado (Indent) para saída legível
Para gerar JSON “bonito” (útil em logs, arquivos e debug), use json.MarshalIndent:
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
b, err := json.MarshalIndent(u, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(b))
Campos opcionais: quando usar ponteiros
omitempty resolve muitos casos, mas há situações em que você precisa diferenciar “campo ausente” de “campo presente com valor zero”. Exemplo: em uma API, age=0 pode ser um valor válido, e “não informado” deve ser diferente. Para isso, use ponteiros:
type Profile struct {
Nickname *string `json:"nickname,omitempty"`
Age *int `json:"age,omitempty"`
}
Se Age for nil, o campo é omitido. Se apontar para 0, o campo aparece como "age": 0.
Campos ignorados e nomes especiais
json:"-"ignora o campo completamente.- Se você não colocar tag, o nome do campo Go vira o nome no JSON (com a mesma grafia).
type Internal struct {
PasswordHash string `json:"-"`
DisplayName string // vira "DisplayName" no JSON
}
Unmarshaling: de JSON para struct
Exemplo básico
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
var u User
err := json.Unmarshal([]byte(`{"id":1,"name":"Ana","created_at":"2026-01-25T10:00:00Z"}`), &u)
if err != nil {
// trate o erro
}
Detalhe sobre time.Time: o JSON precisa trazer a data/hora como string RFC3339 para o parse automático funcionar. Se o formato for diferente, você pode usar um tipo customizado (ver seção de validação e customização).
Tipos embutidos (embedded) e composição
Quando você embute um struct em outro, os campos do embutido “sobem” e podem aparecer no mesmo nível do JSON (dependendo das tags). Isso é útil para compor modelos sem duplicar campos.
type Audit struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Audit // embutido
}
O JSON esperado pode ser:
{
"id": 1,
"name": "Mouse",
"created_at": "2026-01-25T10:00:00Z",
"updated_at": "2026-01-25T12:00:00Z"
}
Campos desconhecidos: boas práticas
Por padrão, o Go ignora campos desconhecidos no JSON ao fazer Unmarshal. Isso pode ser desejável para compatibilidade, mas pode esconder erros (ex.: typo no nome do campo, payload inesperado).
Boas práticas comuns:
- Em integrações críticas (importação de arquivo, payloads que você controla), prefira falhar em campos desconhecidos.
- Em APIs públicas onde você quer tolerância a evolução, pode aceitar campos extras e ignorá-los.
Para falhar em campos desconhecidos, use json.Decoder com DisallowUnknownFields (ver próxima seção).
Decodificação em stream com json.Decoder
json.Unmarshal exige ter todo o JSON em memória (um []byte). Para ler de um stream (arquivo grande, io.Reader, rede), use json.NewDecoder. Isso permite:
- Processar dados sem carregar tudo em memória.
- Configurar comportamento:
DisallowUnknownFields,UseNumber. - Decodificar múltiplos objetos em sequência (JSON concatenado) ou iterar tokens.
Exemplo: decodificar um objeto de um Reader e bloquear campos desconhecidos
dec := json.NewDecoder(r)
dec.DisallowUnknownFields()
var u User
if err := dec.Decode(&u); err != nil {
// erro de sintaxe, tipo incompatível ou campo desconhecido
}
Exemplo: decodificar uma lista grande
Se o JSON for um array grande ([{...},{...},...]), você pode decodificar tudo em um []T (mais simples) ou iterar por tokens (mais avançado). Para iniciantes, o caminho simples com Decoder já ajuda a evitar ler o arquivo inteiro como string:
dec := json.NewDecoder(r)
dec.DisallowUnknownFields()
var users []User
if err := dec.Decode(&users); err != nil {
// trate
}
UseNumber: evitando perda de precisão em números
Quando você decodifica JSON em map[string]any, números viram float64 por padrão. Se você precisa preservar o valor numérico sem converter automaticamente, use UseNumber:
dec := json.NewDecoder(r)
dec.UseNumber()
var v map[string]any
if err := dec.Decode(&v); err != nil {
// trate
}
Assim, números vêm como json.Number, e você decide se converte para int64 ou float64.
Validação de dados após decodificar
O pacote encoding/json valida sintaxe e compatibilidade de tipos (por exemplo, não dá para colocar uma string em um campo int). Porém, regras de negócio (campo obrigatório, faixa de valores, formato de e-mail) são responsabilidade do seu código.
Estratégia prática: função Validate() no seu tipo
type UserInput struct {
Name string `json:"name"`
Email string `json:"email"`
BirthDate time.Time `json:"birth_date"`
}
func (u UserInput) Validate() error {
if len(u.Name) < 2 {
return fmt.Errorf("name deve ter pelo menos 2 caracteres")
}
if u.Email == "" {
return fmt.Errorf("email é obrigatório")
}
if u.BirthDate.IsZero() {
return fmt.Errorf("birth_date é obrigatório")
}
return nil
}
Se você precisa aceitar data em outro formato (ex.: "25/01/2026"), crie um tipo com UnmarshalJSON customizado. Exemplo com data sem horário:
type DateOnly struct{ time.Time }
func (d *DateOnly) UnmarshalJSON(b []byte) error {
// b vem com aspas: "2026-01-25"
s := strings.Trim(string(b), "\"")
if s == "" || s == "null" {
return nil
}
t, err := time.Parse("2006-01-02", s)
if err != nil {
return fmt.Errorf("data inválida (esperado YYYY-MM-DD): %w", err)
}
d.Time = t
return nil
}
func (d DateOnly) MarshalJSON() ([]byte, error) {
if d.Time.IsZero() {
return []byte("null"), nil
}
return []byte("\"" + d.Format("2006-01-02") + "\""), nil
}
Esse padrão é útil quando o JSON não segue RFC3339, mas você ainda quer manter um tipo forte e previsível.
Exercício prático: ler JSON, validar e escrever JSON formatado
Objetivo
Você vai criar um pequeno programa que:
- Lê um arquivo
input.jsoncontendo uma lista de registros. - Decodifica usando
json.DecodercomDisallowUnknownFields. - Valida cada item (campos obrigatórios e regras simples).
- Gera um arquivo
output.jsoncom os itens válidos, incluindo um campo calculado, usandojson.MarshalIndent.
Formato do input.json
[
{
"id": 1,
"name": "Caderno",
"price": 19.9,
"created_at": "2026-01-25T10:00:00Z"
},
{
"id": 2,
"name": "",
"price": -5,
"created_at": "2026-01-25T10:00:00Z"
}
]
Passo 1: modelar os dados
type Item struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
CreatedAt time.Time `json:"created_at"`
}
type OutputItem struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
CreatedAt time.Time `json:"created_at"`
Tag string `json:"tag"`
}
Passo 2: implementar validação
func (it Item) Validate() error {
if it.ID <= 0 {
return fmt.Errorf("id deve ser > 0")
}
if strings.TrimSpace(it.Name) == "" {
return fmt.Errorf("name é obrigatório")
}
if it.Price < 0 {
return fmt.Errorf("price não pode ser negativo")
}
if it.CreatedAt.IsZero() {
return fmt.Errorf("created_at é obrigatório")
}
return nil
}
Passo 3: ler, decodificar com Decoder e bloquear campos desconhecidos
f, err := os.Open("input.json")
if err != nil {
return err
}
defer f.Close()
dec := json.NewDecoder(f)
dec.DisallowUnknownFields()
var items []Item
if err := dec.Decode(&items); err != nil {
return fmt.Errorf("falha ao decodificar JSON: %w", err)
}
Passo 4: validar e transformar para saída
valid := make([]OutputItem, 0, len(items))
for _, it := range items {
if err := it.Validate(); err != nil {
// neste exercício, apenas ignore inválidos (ou acumule erros)
continue
}
tag := "ok"
if it.Price == 0 {
tag = "free"
}
valid = append(valid, OutputItem{
ID: it.ID,
Name: it.Name,
Price: it.Price,
CreatedAt: it.CreatedAt,
Tag: tag,
})
}
Passo 5: escrever output.json com JSON formatado
out, err := json.MarshalIndent(valid, "", " ")
if err != nil {
return fmt.Errorf("falha ao gerar JSON: %w", err)
}
if err := os.WriteFile("output.json", out, 0644); err != nil {
return fmt.Errorf("falha ao escrever output.json: %w", err)
}
Desafios extras (opcionais)
- Em vez de ignorar inválidos, gere um segundo arquivo
errors.jsoncomide mensagem de erro. - Troque
float64por um tipo de dinheiro (ex.: centavos emint64) e ajuste o JSON para evitar problemas de precisão. - Faça o programa aceitar os nomes dos arquivos via argumentos e recusar JSON com múltiplos objetos (verificando se há tokens extras após o
Decode).