JSON Formatter Go — Guia do MarshalIndent()
Use o Formatador e Embelezador JSON gratuito diretamente no seu navegador — sem instalação.
Experimentar Formatador e Embelezador JSON online →Quando estou trabalhando em um microsserviço Go e preciso inspecionar uma resposta de API ou um arquivo de configuração, o JSON compacto é o primeiro obstáculo — uma única linha com centenas de campos aninhados não me diz quase nada à primeira vista. Para formatar JSON em Go, a biblioteca padrão oferece tudo que você precisa: json.MarshalIndent está embutido em encoding/json, vem com cada instalação do Go e não requer dependências externas. Este guia cobre o quadro completo: struct tags, implementações personalizadas de MarshalJSON() , json.Indent para reformatar bytes brutos, streaming de arquivos grandes com json.Decoder, e quando recorrer a go-json para caminhos de alto desempenho, além de comandos de linha única para formatar rapidamente no terminal. Todos os exemplos usam Go 1.21+.
- ✓json.MarshalIndent(v, "", "\t") é da biblioteca padrão — zero dependências, incluído em cada instalação do Go.
- ✓Struct tags json:"field_name,omitempty" controlam as chaves de serialização e omitem campos com valor zero.
- ✓Implemente MarshalJSON() em qualquer tipo para controlar completamente sua representação JSON.
- ✓json.Indent() reformata []byte já serializados sem re-parsear a struct — mais rápido para bytes brutos.
- ✓Para arquivos grandes (>100 MB): use json.Decoder com Token() para streaming sem carregar tudo na memória.
- ✓go-json é um substituto direto 3–5× mais rápido que encoding/json para APIs de alto throughput.
O que é formatação de JSON?
A formatação de JSON — também chamada de pretty-printing — transforma uma string JSON compacta e minificada em um layout legível com indentação e quebras de linha consistentes. Os dados subjacentes são idênticos; apenas os espaços em branco mudam. O JSON compacto é ideal para transferência de rede onde cada byte importa; o JSON formatado é ideal para depuração, revisão de código, inspeção de logs e edição de arquivos de configuração. O pacote encoding/json do Go lida com ambos os modos com uma única chamada de função — alterne entre saída compacta e indentada escolhendo entre json.Marshal e json.MarshalIndent.
{"service":"payments","port":8443,"workers":4}{
"service": "payments",
"port": 8443,
"workers": 4
}json.MarshalIndent() — A abordagem da biblioteca padrão
json.MarshalIndent faz parte do pacote encoding/json , que integra a biblioteca padrão do Go — nenhum go get necessário. Sua assinatura é MarshalIndent(v any, prefix, indent string) ([]byte, error): a string prefix é anteposta a cada linha de saída (quase sempre deixada vazia), e indent é repetida uma vez por nível de aninhamento. Passe "\t" para tabulações ou " " para dois espaços.
package main
import (
"encoding/json"
"fmt"
"log"
)
type ServiceConfig struct {
Host string `json:"host"`
Port int `json:"port"`
Workers int `json:"workers"`
TLSEnabled bool `json:"tls_enabled"`
AllowedIPs []string `json:"allowed_ips"`
}
func main() {
cfg := ServiceConfig{
Host: "payments-api.internal",
Port: 8443,
Workers: 8,
TLSEnabled: true,
AllowedIPs: []string{"10.0.0.0/8", "172.16.0.0/12"},
}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
log.Fatalf("marshal config: %v", err)
}
fmt.Println(string(data))
}
// {
// "host": "payments-api.internal",
// "port": 8443,
// "workers": 8,
// "tls_enabled": true,
// "allowed_ips": [
// "10.0.0.0/8",
// "172.16.0.0/12"
// ]
// }A escolha entre tabulações e espaços é principalmente uma convenção de equipe. Muitos projetos Go preferem tabulações porque gofmt (que formata código-fonte Go) as utiliza. Indentação de dois ou quatro espaços é comum quando o JSON é destinado a um consumidor JavaScript ou Python. Aqui está a mesma struct com ambos os estilos de indentação lado a lado:
// Indentação com tabulações — preferida em ferramentas nativas do Go
data, _ := json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }
// Indentação com dois espaços — comum para APIs com consumidores JS/Python
data, _ = json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }
// Indentação com quatro espaços
data, _ = json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }json.Marshal(v) quando precisar de saída compacta — payloads de rede, valores em cache ou qualquer caminho onde o tamanho em bytes importa. Aceita o mesmo argumento de valor e tem a mesma semântica de erro, mas produz JSON em uma única linha sem espaços em branco.Struct Tags — Controlando nomes de campos e omitempty
Struct tags do Go são literais de string colocadas após declarações de campo que informam aencoding/jsoncomo serializar cada campo. Existem três diretivas principais: json:"name" renomeia o campo na saída, omitempty omite o campo quando contém o valor zero do seu tipo, e json:"-" exclui o campo completamente — útil para senhas, identificadores internos ou campos que nunca devem cruzar o limite do serviço.
type UserProfile struct {
ID string `json:"id"`
Email string `json:"email"`
DisplayName string `json:"display_name,omitempty"` // omitir se string vazia
AvatarURL *string `json:"avatar_url,omitempty"` // omitir se ponteiro nil
IsAdmin bool `json:"is_admin,omitempty"` // omitir se false
passwordHash string // não exportado — excluído automaticamente
}
// Usuário com todos os campos opcionais preenchidos
full := UserProfile{
ID: "usr_7b3c", Email: "j.silva@exemplo.com",
DisplayName: "João Silva", IsAdmin: true,
}
// {
// "id": "usr_7b3c",
// "email": "j.silva@exemplo.com",
// "display_name": "João Silva",
// "is_admin": true
// }
// Usuário sem campos opcionais — são omitidos por completo
minimal := UserProfile{ID: "usr_2a91", Email: "c.rocha@exemplo.com"}
// {
// "id": "usr_2a91",
// "email": "c.rocha@exemplo.com"
// }A tag json:"-" é a escolha certa para campos que devem ser excluídos incondicionalmente independentemente do seu valor — tipicamente segredos, campos de rastreamento interno ou dados corretos em memória que nunca devem ser serializados para qualquer sistema externo.
type AuthToken struct {
TokenID string `json:"token_id"`
Subject string `json:"sub"`
IssuedAt int64 `json:"iat"`
ExpiresAt int64 `json:"exp"`
SigningKey []byte `json:"-"` // nunca serializado
RefreshToken string `json:"-"` // nunca serializado
}
tok := AuthToken{
TokenID: "tok_8f2a", Subject: "usr_7b3c",
IssuedAt: 1741614120, ExpiresAt: 1741617720,
SigningKey: []byte("segredo"), RefreshToken: "rt_9e4f",
}
data, _ := json.MarshalIndent(tok, "", " ")
// {
// "token_id": "tok_8f2a",
// "sub": "usr_7b3c",
// "iat": 1741614120,
// "exp": 1741617720
// }
// SigningKey e RefreshToken nunca aparecemencoding/json independentemente de qualquer tag. Não é necessário adicionar json:"-" a campos não exportados — a exclusão é automática e não pode ser substituída.MarshalJSON() personalizado — Tratando tipos não padrão
Qualquer tipo Go pode implementar a interface json.Marshaler definindo um método MarshalJSON() ([]byte, error). Quando encoding/json encontra tal tipo, chama o método em vez de sua serialização padrão baseada em reflexão. Este é o padrão Go canônico para tipos de domínio que precisam de uma representação de rede específica — valores monetários, enums de status, formatos de data personalizados ou qualquer tipo que armazene dados de forma diferente de como deve ser serializado.
Tipo personalizado — Money com conversão de centavos para decimal
package main
import (
"encoding/json"
"fmt"
"log"
)
type Money struct {
Amount int64 // armazenado em centavos para evitar imprecisão de ponto flutuante
Currency string
}
func (m Money) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Amount float64 `json:"amount"`
Currency string `json:"currency"`
Display string `json:"display"`
}{
Amount: float64(m.Amount) / 100,
Currency: m.Currency,
Display: fmt.Sprintf("%s %.2f", m.Currency, float64(m.Amount)/100),
})
}
type Invoice struct {
ID string `json:"id"`
Subtotal Money `json:"subtotal"`
Tax Money `json:"tax"`
Total Money `json:"total"`
}
func main() {
inv := Invoice{
ID: "inv_9a2f91bc",
Subtotal: Money{Amount: 19900, Currency: "BRL"},
Tax: Money{Amount: 1592, Currency: "BRL"},
Total: Money{Amount: 21492, Currency: "BRL"},
}
data, err := json.MarshalIndent(inv, "", " ")
if err != nil {
log.Fatalf("marshal invoice: %v", err)
}
fmt.Println(string(data))
}
// {
// "id": "inv_9a2f91bc",
// "subtotal": { "amount": 199, "currency": "BRL", "display": "BRL 199.00" },
// "tax": { "amount": 15.92, "currency": "BRL", "display": "BRL 15.92" },
// "total": { "amount": 214.92, "currency": "BRL", "display": "BRL 214.92" }
// }Enum de status — Representação como string
type OrderStatus int
const (
StatusPending OrderStatus = iota
StatusPaid
StatusShipped
StatusCancelled
)
var orderStatusNames = map[OrderStatus]string{
StatusPending: "pendente",
StatusPaid: "pago",
StatusShipped: "enviado",
StatusCancelled: "cancelado",
}
func (s OrderStatus) MarshalJSON() ([]byte, error) {
name, ok := orderStatusNames[s]
if !ok {
return nil, fmt.Errorf("status de pedido desconhecido: %d", s)
}
return json.Marshal(name)
}
type Order struct {
ID string `json:"id"`
Status OrderStatus `json:"status"`
}
o := Order{ID: "ord_3c7f", Status: StatusShipped}
data, _ := json.MarshalIndent(o, "", " ")
// {
// "id": "ord_3c7f",
// "status": "enviado"
// }MarshalJSON() e UnmarshalJSON() juntos. Se você implementar apenas a serialização, fazer um ciclo completo do tipo por JSON (serializar → armazenar → desserializar) perderá estrutura silenciosamente ou retornará o tipo errado. O par forma um contrato que garante que o tipo pode sobreviver a um ciclo completo de JSON.UUID — Serializar como string
A biblioteca padrão do Go não tem tipo UUID. A escolha mais comum é github.com/google/uuid, que já implementa MarshalJSON() e serializa como uma string RFC 4122 entre aspas. Se você usar um [16]byte bruto ou um tipo ID personalizado, implemente a interface você mesmo para evitar blobs binários codificados em base64 na sua saída JSON.
import (
"encoding/json"
"fmt"
"github.com/google/uuid"
)
type AuditEvent struct {
EventID uuid.UUID `json:"event_id"` // serializa como "550e8400-e29b-41d4-a716-446655440000"
SessionID uuid.UUID `json:"session_id"`
Action string `json:"action"`
ActorID string `json:"actor_id"`
OccuredAt string `json:"occurred_at"`
}
event := AuditEvent{
EventID: uuid.New(),
SessionID: uuid.MustParse("550e8400-e29b-41d4-a716-446655440000"),
Action: "usuario.senha_alterada",
ActorID: "usr_7f3a91bc",
OccuredAt: "2026-03-10T14:22:00Z",
}
data, _ := json.MarshalIndent(event, "", " ")
fmt.Println(string(data))
// {
// "event_id": "a4b2c1d0-...",
// "session_id": "550e8400-e29b-41d4-a716-446655440000",
// "action": "usuario.senha_alterada",
// "actor_id": "usr_7f3a91bc",
// "occurred_at": "2026-03-10T14:22:00Z"
// }[16]byte sem um tipo personalizado, encoding/json os codifica como string base64 — ex.: "VQ6EAOKbQdSnFkRmVUQAAA==". Use sempre um tipo UUID adequado ou implemente MarshalJSON() para emitir o formato canônico de string com hífens.Referência de parâmetros de json.MarshalIndent()
A assinatura da função é func MarshalIndent(v any, prefix, indent string) ([]byte, error). Tanto prefix quanto indent são literais de string — não há atalho numérico como o indent=4 do Python.
Opções comuns de struct tags:
json.Indent() — Reformatar bytes JSON existentes
Quando você já tem um []byte de JSON — por exemplo do corpo de uma resposta HTTP, uma coluna jsonb do Postgres, ou um arquivo lido com os.ReadFile — não precisa definir uma struct nem fazer unmarshal antes de formatar. json.Indent reformata diretamente os bytes brutos escrevendo a saída indentada em um bytes.Buffer.
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
)
func main() {
// Simulando um payload JSON bruto de um serviço upstream
raw := []byte(`{"trace_id":"tr_9a2f","service":"checkout","latency_ms":342,"error":null}`)
var buf bytes.Buffer
if err := json.Indent(&buf, raw, "", " "); err != nil {
log.Fatalf("indent: %v", err)
}
fmt.Println(buf.String())
}
// {
// "trace_id": "tr_9a2f",
// "service": "checkout",
// "latency_ms": 342,
// "error": null
// }Um padrão que uso frequentemente em microsserviços é chamar json.Indent antes de escrever em logs estruturados — adiciona overhead mínimo e torna as entradas de log muito mais fáceis de ler durante um incidente. A função é especialmente útil para registrar respostas HTTP, formatar strings JSON armazenadas e pipelines de formato-na-leitura onde a definição da struct não está disponível.
func logResponse(logger *slog.Logger, statusCode int, body []byte) {
var pretty bytes.Buffer
if err := json.Indent(&pretty, body, "", " "); err != nil {
// Corpo não é JSON válido — registrar bruto
logger.Debug("resposta upstream", "status", statusCode, "body", string(body))
return
}
logger.Debug("resposta upstream", "status", statusCode, "body", pretty.String())
}json.Indent NÃO valida completamente o JSON além do que é estruturalmente necessário para inserir espaços em branco. Para validação sintática completa, chame json.Valid(data) primeiro e trate o caso false antes de tentar a indentação.Formatar JSON de um arquivo e de uma resposta HTTP
Dois dos cenários mais comuns em serviços Go são formatar JSON lido de um arquivo em disco (arquivos de configuração, dados de fixtures, seeds de migração) e formatar corpos de resposta HTTP para logging de depuração ou asserções em testes. Ambos seguem o mesmo padrão: ler bytes, chamar json.Indent ou fazer unmarshal e depois json.MarshalIndent, escrever de volta ou registrar.
Ler arquivo → Formatar → Escrever de volta
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"os"
)
func formatJSONFile(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("ler %s: %w", path, err)
}
if !json.Valid(data) {
return fmt.Errorf("JSON inválido em %s", path)
}
var buf bytes.Buffer
if err := json.Indent(&buf, data, "", " "); err != nil {
return fmt.Errorf("indent %s: %w", path, err)
}
if err := os.WriteFile(path, buf.Bytes(), 0644); err != nil {
return fmt.Errorf("escrever %s: %w", path, err)
}
return nil
}
func main() {
if err := formatJSONFile("config/database.json"); err != nil {
log.Fatalf("formatar config: %v", err)
}
fmt.Println("config/database.json formatado com sucesso")
}Resposta HTTP → Decodificar → Formatar para logging de depuração
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
type HealthResponse struct {
Status string `json:"status"`
Version string `json:"version"`
Checks map[string]string `json:"checks"`
UptimeSec int64 `json:"uptime_seconds"`
}
func main() {
resp, err := http.Get("https://api.payments.internal/v2/health")
if err != nil {
log.Fatalf("health check: %v", err)
}
defer resp.Body.Close()
var result HealthResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
log.Fatalf("decodificar resposta health: %v", err)
}
pretty, err := json.MarshalIndent(result, "", " ")
if err != nil {
log.Fatalf("marshal resposta health: %v", err)
}
fmt.Printf("Health check (%d):
%s
", resp.StatusCode, string(pretty))
}
// Health check (200):
// {
// "status": "ok",
// "version": "1.4.2",
// "checks": {
// "database": "ok",
// "cache": "ok",
// "queue": "degraded"
// },
// "uptime_seconds": 172800
// }Corpo de resposta bruto → json.Indent (sem struct necessária)
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
)
func main() {
resp, err := http.Get("https://api.payments.internal/v2/health")
if err != nil {
log.Fatalf("request: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalf("ler corpo: %v", err)
}
var buf bytes.Buffer
if err := json.Indent(&buf, body, "", " "); err != nil {
log.Fatalf("indent: %v", err)
}
fmt.Println(buf.String())
}Exibir JSON formatado de uma resposta HTTP em Go
As duas abordagens acima cobrem os casos mais comuns: decodificar em uma struct tipada e depois chamar json.MarshalIndent (melhor quando você precisa validar ou inspecionar campos), ou ler os bytes brutos do corpo com io.ReadAll e chamar diretamente json.Indent (melhor para logging de depuração rápida quando você não tem uma definição de struct à mão). A abordagem de bytes brutos é mais simples, mas não fornece segurança de tipos Go nem acesso a campos — é puramente uma transformação de exibição. Ambas as abordagens tratam corpos de resposta grandes corretamente, desde que o corpo completo caiba na memória.
// Padrão A: decodificação tipada → MarshalIndent // Use quando precisar inspecionar ou validar campos específicos var result map[string]any json.NewDecoder(resp.Body).Decode(&result) pretty, _ := json.MarshalIndent(result, "", " ") fmt.Println(string(pretty)) // Padrão B: bytes brutos → json.Indent // Use para logging de depuração rápida — sem definição de struct necessária body, _ := io.ReadAll(resp.Body) var buf bytes.Buffer json.Indent(&buf, body, "", " ") fmt.Println(buf.String())
Formatação de JSON por linha de comando em projetos Go
Às vezes você precisa formatar um payload JSON direto no terminal sem escrever um programa Go. Estes comandos de linha única são os que tenho na memória muscular durante o desenvolvimento e a resposta a incidentes.
echo '{"service":"payments","port":8443,"workers":4}' | python3 -m json.tool
# {
# "service": "payments",
# "port": 8443,
# "workers": 4
# }# Apenas formatar cat api-response.json | jq . # Extrair um campo aninhado cat api-response.json | jq '.checks.database' # Filtrar um array cat audit-log.json | jq '.[] | select(.severity == "error")'
# main.go: lê stdin, formata, escreve stdout
cat <<'EOF' > /tmp/fmt.go
package main
import ("bytes";"encoding/json";"io";"os")
func main() {
b,_:=io.ReadAll(os.Stdin)
var buf bytes.Buffer
json.Indent(&buf,b,""," ")
os.Stdout.Write(buf.Bytes())
}
EOF
echo '{"port":8080,"debug":true}' | go run /tmp/fmt.gogofmt formata código-fonte Go, não JSON. Não passe arquivos JSON por gofmt — ele produzirá um erro ou saída irreconhecível. Use jq . ou python3 -m json.tool para arquivos JSON.Alternativa de alto desempenho — go-json
Para a grande maioria dos serviços Go, encoding/json é rápido o suficiente. Mas se a serialização JSON aparecer no seu profiler — comum em APIs REST de alto throughput ou serviços que emitem linhas de log estruturadas grandes em cada requisição — a biblioteca go-json é um substituto direto que é 3–5× mais rápido com superfície de API idêntica.
go get github.com/goccy/go-json
package main
import (
// Substitua isso:
// "encoding/json"
// Por isso — API idêntica, sem outras mudanças de código:
json "github.com/goccy/go-json"
"fmt"
"log"
)
type AuditEvent struct {
RequestID string `json:"request_id"`
UserID string `json:"user_id"`
Action string `json:"action"`
ResourceID string `json:"resource_id"`
IPAddress string `json:"ip_address"`
DurationMs int `json:"duration_ms"`
}
func main() {
event := AuditEvent{
RequestID: "req_7d2e91", UserID: "usr_4421",
Action: "fatura.download", ResourceID: "inv_9a2f",
IPAddress: "203.0.113.45", DurationMs: 23,
}
data, err := json.MarshalIndent(event, "", " ")
if err != nil {
log.Fatalf("marshal: %v", err)
}
fmt.Println(string(data))
// A saída é idêntica a encoding/json — apenas a velocidade difere
}github.com/bytedance/sonic é a biblioteca JSON mais rápida disponível para Go, mas funciona apenas em amd64 e arm64 (usa compilação JIT). Use go-json quando precisar de um substituto portável; recorra a sonic quando estiver em uma arquitetura conhecida e precisar de cada microssegundo em um caminho crítico.
Trabalhando com arquivos JSON grandes
Tanto json.MarshalIndent quanto json.Indent requerem que o payload completo esteja na memória. Para arquivos acima de 100 MB — exportações de dados, logs de auditoria, payloads de consumidores Kafka — use json.Decoder para fazer streaming da entrada e processar registros um por vez.
Streaming de um array JSON grande com json.Decoder
package main
import (
"encoding/json"
"fmt"
"log"
"os"
)
type AuditEvent struct {
RequestID string `json:"request_id"`
UserID string `json:"user_id"`
Action string `json:"action"`
Severity string `json:"severity"`
DurationMs int `json:"duration_ms"`
}
func processAuditLog(path string) error {
file, err := os.Open(path)
if err != nil {
return fmt.Errorf("abrir %s: %w", path, err)
}
defer file.Close()
dec := json.NewDecoder(file)
// Ler o '[' de abertura
if _, err := dec.Token(); err != nil {
return fmt.Errorf("ler token de abertura: %w", err)
}
var processed int
for dec.More() {
var event AuditEvent
if err := dec.Decode(&event); err != nil {
return fmt.Errorf("decodificar evento %d: %w", processed, err)
}
// Processar um evento por vez — uso de memória constante
if event.Severity == "error" {
fmt.Printf("[ERROR] %s: %s (%dms)
", event.UserID, event.Action, event.DurationMs)
}
processed++
}
fmt.Printf("Processados %d eventos de auditoria
", processed)
return nil
}
func main() {
if err := processAuditLog("audit-2026-03.json"); err != nil {
log.Fatalf("processar log de auditoria: %v", err)
}
}NDJSON — Um objeto JSON por linha
package main
import (
"bufio"
"encoding/json"
"fmt"
"log"
"os"
)
type LogLine struct {
Timestamp string `json:"ts"`
Level string `json:"level"`
Service string `json:"service"`
Message string `json:"msg"`
TraceID string `json:"trace_id"`
DurationMs int `json:"duration_ms,omitempty"`
}
func main() {
file, err := os.Open("service-2026-03.ndjson")
if err != nil {
log.Fatalf("abrir log: %v", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024) // 1 MB por linha
for scanner.Scan() {
var line LogLine
if err := json.Unmarshal(scanner.Bytes(), &line); err != nil {
continue // ignorar linhas malformadas
}
if line.Level == "error" {
fmt.Printf("%s [%s] %s trace=%s
",
line.Timestamp, line.Service, line.Message, line.TraceID)
}
}
if err := scanner.Err(); err != nil {
log.Fatalf("scan: %v", err)
}
}os.ReadFile alocará todo esse buffer no heap, gerará pressão no GC e pode causar OOM em containers com memória limitada.Erros comuns
Problema: Descartar o erro com _ significa que um valor não serializável (canal, função, número complexo) produz silenciosamente nil como saída ou causa um panic mais adiante ao chamar string(nil).
Solução: Sempre verifique o erro. Se você estiver serializando um tipo que sempre deveria ter sucesso, um log.Fatalf com panic é melhor do que perda silenciosa de dados.
data, _ := json.MarshalIndent(payload, "", " ") fmt.Println(string(data)) // string vazia se o marshal falhou
data, err := json.MarshalIndent(payload, "", " ")
if err != nil {
log.Fatalf("marshal payload: %v", err)
}
fmt.Println(string(data))Problema: fmt.Println(string(data)) adiciona um caractere de nova linha ao final do JSON, o que corrompe pipelines que tratam a saída como bytes brutos — por exemplo, ao passar para jq ou escrever em um protocolo binário.
Solução: Use os.Stdout.Write(data) para saída binária limpa. Se precisar de uma nova linha ao final para exibição humana, adicione-a explicitamente.
data, _ := json.MarshalIndent(cfg, "", " ") fmt.Println(string(data)) // adiciona uma nova linha extra ao final
data, _ := json.MarshalIndent(cfg, "", " ")
os.Stdout.Write(data)
os.Stdout.Write([]byte("
")) // nova linha explícita apenas quando necessárioProblema: Sem omitempty, um ponteiro nil *string ou *int é serializado como "field": null. Isso expõe nomes de campos internos e pode quebrar validadores de esquema JSON estritos no lado do consumidor.
Solução: Adicione omitempty a campos ponteiro que você quer ausentes (não null) na saída. Um *T nil com omitempty não produz nenhuma chave no JSON.
type WebhookPayload struct {
EventID string `json:"event_id"`
ErrorMsg *string `json:"error_msg"` // aparece como null quando nil
}
// {"event_id":"evt_3c7f","error_msg":null}type WebhookPayload struct {
EventID string `json:"event_id"`
ErrorMsg *string `json:"error_msg,omitempty"` // omitido quando nil
}
// {"event_id":"evt_3c7f"}Problema: Fazer unmarshal em map[string]any perde informação de tipos, requer type assertions manuais e produz ordem de chaves não determinística — tornando diffs de JSON e comparações de log mais difíceis.
Solução: Defina uma struct com json tags adequados. Structs são type-safe, serializam mais rápido, produzem ordem de campos determinística correspondendo à definição da struct e tornam o código autodocumentado.
var result map[string]any json.Unmarshal(body, &result) port := result["port"].(float64) // type assertion necessária, causa panic se o tipo estiver errado
type ServiceStatus struct {
Service string `json:"service"`
Port int `json:"port"`
Healthy bool `json:"healthy"`
}
var result ServiceStatus
json.Unmarshal(body, &result)
port := result.Port // tipado, seguro, rápidoencoding/json vs alternativas — Comparação rápida
Use json.MarshalIndent para qualquer caso onde você controla a definição da struct e precisa de saída formatada — arquivos de configuração, logging de depuração, fixtures de testes e logging de respostas de API. Use json.Indent quando você já tem bytes brutos e apenas precisa adicionar espaços sem ida e volta por tipos Go. Mude para go-json ou sonic somente após o profiling confirmar que a serialização JSON é um gargalo mensurável — para a maioria dos serviços, a biblioteca padrão é mais do que suficiente.
Perguntas frequentes
Como exibo JSON formatado em Go?
Chame json.MarshalIndent(v, "", "\t") do pacote encoding/json — o segundo argumento é um prefixo por linha (normalmente vazio) e o terceiro é a indentação por nível. Passe "\t" para tabulações ou " " para dois espaços. Nenhuma biblioteca externa é necessária; encoding/json faz parte da biblioteca padrão do Go.
package main
import (
"encoding/json"
"fmt"
"log"
)
func main() {
config := map[string]any{
"service": "payments-api",
"port": 8443,
"region": "sa-east-1",
}
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
log.Fatalf("marshal: %v", err)
}
fmt.Println(string(data))
// {
// "port": 8443,
// "region": "sa-east-1",
// "service": "payments-api"
// }
}Qual é a diferença entre json.Marshal e json.MarshalIndent?
json.Marshal produz JSON compacto em uma única linha sem espaços em branco — ideal para transferência de rede onde cada byte importa. json.MarshalIndent aceita dois parâmetros extras (prefix e indent) e produz saída indentada e legível. Ambas as funções aceitam os mesmos tipos de valor e retornam ([]byte, error). O único custo de MarshalIndent são alguns bytes a mais na saída e um mínimo de CPU extra para inserir os espaços.
import "encoding/json"
type HealthCheck struct {
Status string `json:"status"`
Version string `json:"version"`
Uptime int `json:"uptime_seconds"`
}
h := HealthCheck{Status: "ok", Version: "1.4.2", Uptime: 86400}
compact, _ := json.Marshal(h)
// {"status":"ok","version":"1.4.2","uptime_seconds":86400}
pretty, _ := json.MarshalIndent(h, "", " ")
// {
// "status": "ok",
// "version": "1.4.2",
// "uptime_seconds": 86400
// }Como formato um []byte de JSON sem fazer unmarshal da struct?
Use json.Indent(&buf, src, "", "\t"). Esta função recebe um []byte de JSON existente e escreve a versão indentada em um bytes.Buffer — sem definir uma struct, sem type assertion e sem ida e volta por tipos Go. É a opção mais rápida quando você já tem bytes JSON brutos, por exemplo do corpo de uma resposta HTTP ou de uma coluna de banco de dados.
import (
"bytes"
"encoding/json"
"fmt"
"log"
)
raw := []byte(`{"endpoint":"/api/v2/faturas","page":1,"per_page":50,"total":312}`)
var buf bytes.Buffer
if err := json.Indent(&buf, raw, "", " "); err != nil {
log.Fatalf("indent: %v", err)
}
fmt.Println(buf.String())
// {
// "endpoint": "/api/v2/faturas",
// "page": 1,
// "per_page": 50,
// "total": 312
// }Por que json.MarshalIndent retorna um erro?
encoding/json retorna um erro quando o valor não pode ser representado como JSON. As causas mais comuns são: serializar um canal, uma função ou um número complexo (esses tipos não têm equivalente JSON); uma struct que implementa MarshalJSON() e retorna um erro; ou um map com chaves que não são string. Importante: serializar uma struct com campo não exportado ou ponteiro nil NÃO causa erro — eles são simplesmente omitidos.
import (
"encoding/json"
"fmt"
)
// Isso retornará um erro — canais não são serializáveis como JSON
ch := make(chan int)
_, err := json.MarshalIndent(ch, "", " ")
fmt.Println(err)
// json: unsupported type: chan int
// Isso está correto — campos ponteiro nil com omitempty são omitidos silenciosamente
type Profile struct {
ID string `json:"id"`
Avatar *string `json:"avatar,omitempty"`
}
p := Profile{ID: "usr_7b3c"}
data, _ := json.MarshalIndent(p, "", " ")
// {"id": "usr_7b3c"} — avatar omitido, sem erroComo excluo um campo da saída JSON em Go?
Existem três formas. Primeira, use o struct tag json:"-" — o campo sempre é excluído independentemente do seu valor. Segunda, use omitempty — o campo é excluído apenas quando contém o valor zero do seu tipo (ponteiro nil, string vazia, 0, false). Terceira, campos não exportados (em minúsculo) são excluídos automaticamente por encoding/json sem necessidade de tag.
type PaymentMethod struct {
ID string `json:"id"`
Last4 string `json:"last4"`
ExpiryMonth int `json:"expiry_month"`
ExpiryYear int `json:"expiry_year"`
CVV string `json:"-"` // sempre excluído
BillingName string `json:"billing_name,omitempty"` // excluído se vazio
internalRef string // não exportado — excluído automaticamente
}
pm := PaymentMethod{
ID: "pm_9f3a", Last4: "4242",
ExpiryMonth: 12, ExpiryYear: 2028,
CVV: "123", internalRef: "stripe:pm_9f3a",
}
data, _ := json.MarshalIndent(pm, "", " ")
// CVV, BillingName (vazio) e internalRef não aparecem na saídaComo trato time.Time na serialização JSON?
encoding/json serializa time.Time no formato RFC3339Nano por padrão (ex.: "2026-03-10T14:22:00Z"), compatível com ISO 8601. Se precisar de um formato diferente — como inteiros Unix epoch para uma API legada, ou uma string apenas com a data — implemente MarshalJSON() em um tipo wrapper que embuta time.Time e retorne o formato necessário.
import (
"encoding/json"
"fmt"
"time"
)
// Comportamento padrão — RFC3339Nano, sem código personalizado
type AuditEvent struct {
Action string `json:"action"`
OccurredAt time.Time `json:"occurred_at"`
}
e := AuditEvent{
Action: "fatura.paga",
OccurredAt: time.Date(2026, 3, 10, 14, 22, 0, 0, time.UTC),
}
data, _ := json.MarshalIndent(e, "", " ")
// {
// "action": "fatura.paga",
// "occurred_at": "2026-03-10T14:22:00Z"
// }
// Personalizado: timestamp Unix como inteiro
type UnixTime struct{ time.Time }
func (u UnixTime) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("%d", u.Unix())), nil
}Ferramentas relacionadas
James is a systems engineer and Go enthusiast who focuses on high-performance microservices, command-line tooling, and infrastructure automation. He enjoys the simplicity and explicitness of Go and writes about building fast, reliable backend systems. When not coding he explores distributed systems concepts and contributes to open-source Go libraries.
Tobias is a platform engineer who builds developer tooling and internal infrastructure in Go. He has authored several open-source CLI tools and contributes to the Go toolchain ecosystem. He writes about the cobra and urfave/cli frameworks, cross-platform binary distribution, configuration management, and the patterns that make Go an ideal language for building reliable, self-contained command-line utilities.