JSON em Go: encoding/json, marshaling, unmarshaling e validação

Capítulo 13

Tempo estimado de leitura: 8 minutos

+ Exercício

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").
  • omitempty omite 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:

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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.json contendo uma lista de registros.
  • Decodifica usando json.Decoder com DisallowUnknownFields.
  • Valida cada item (campos obrigatórios e regras simples).
  • Gera um arquivo output.json com os itens válidos, incluindo um campo calculado, usando json.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.json com id e mensagem de erro.
  • Troque float64 por um tipo de dinheiro (ex.: centavos em int64) 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).

Agora responda o exercício sobre o conteúdo:

Ao decodificar JSON para uma struct em Go, quando é mais indicado configurar o decoder para falhar em campos desconhecidos (DisallowUnknownFields)?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Por padrão, campos desconhecidos são ignorados no Unmarshal. Em cenários críticos, é melhor falhar para detectar payloads inesperados ou erros de nomes de campos. Para isso, use json.Decoder com DisallowUnknownFields.

Próximo capitúlo

Concorrência em Go: goroutines, channels e padrões de sincronização

Arrow Right Icon
Capa do Ebook gratuito Go (Golang) para Iniciantes: Fundamentos, Concorrência e Estrutura de Projetos
72%

Go (Golang) para Iniciantes: Fundamentos, Concorrência e Estrutura de Projetos

Novo curso

18 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.