JSON Formatter Go — Guida MarshalIndent()
Usa il Formattatore e Abbellitore JSON gratuito direttamente nel tuo browser — nessuna installazione.
Prova Formattatore e Abbellitore JSON online →Quando lavoro su un microservizio Go e ho bisogno di ispezionare una risposta API o un file di configurazione, il JSON compatto è il primo ostacolo — una singola riga con centinaia di campi annidati non mi dice quasi nulla a colpo d'occhio. Per formattare JSON in Go, la libreria standard offre tutto il necessario: json.MarshalIndent è integrato in encoding/json, incluso in ogni installazione di Go e non richiede dipendenze esterne. Questa guida copre il quadro completo: struct tags, implementazioni personalizzate di MarshalJSON() , json.Indent per riformattare byte grezzi, streaming di file grandi con json.Decoder, e quando ricorrere a go-json per percorsi ad alto throughput, oltre a comandi one-liner per la formattazione rapida nel terminale. Tutti gli esempi usano Go 1.21+.
- ✓json.MarshalIndent(v, "", "\t") è della libreria standard — zero dipendenze, incluso in ogni installazione Go.
- ✓I struct tag json:"field_name,omitempty" controllano le chiavi di serializzazione e omettono i campi con valore zero.
- ✓Implementa MarshalJSON() su qualsiasi tipo per controllare completamente la sua rappresentazione JSON.
- ✓json.Indent() riformatta []byte già serializzati senza ri-parsare la struct — più veloce per i byte grezzi.
- ✓Per file grandi (>100 MB): usa json.Decoder con Token() per lo streaming senza caricare tutto in memoria.
- ✓go-json è un sostituto diretto 3–5× più veloce di encoding/json per API ad alto throughput.
Cos'è la formattazione JSON?
La formattazione JSON — chiamata anche pretty-printing — trasforma una stringa JSON compatta e minificata in un layout leggibile con indentazione e interruzioni di riga coerenti. I dati sottostanti sono identici; cambia solo lo spazio bianco. Il JSON compatto è ottimale per il trasferimento di rete dove ogni byte conta; il JSON formattato è ottimale per il debug, la revisione del codice, l'ispezione dei log e la creazione di file di configurazione. Il package encoding/json di Go gestisce entrambe le modalità con una singola chiamata a funzione — alterna tra output compatto e indentato scegliendo tra json.Marshal e json.MarshalIndent.
{"service":"payments","port":8443,"workers":4}{
"service": "payments",
"port": 8443,
"workers": 4
}json.MarshalIndent() — L'approccio della libreria standard
json.MarshalIndent si trova nel package encoding/json , che fa parte della libreria standard di Go — nessun go get necessario. La sua firma è MarshalIndent(v any, prefix, indent string) ([]byte, error): la stringa prefix viene anteposta a ogni riga di output (quasi sempre lasciata vuota), e indent viene ripetuta una volta per livello di annidamento. Passa "\t" per le tabulazioni o " " per due spazi.
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"
// ]
// }La scelta tra tabulazioni e spazi è principalmente una convenzione di team. Molti progetti Go preferiscono le tabulazioni perché gofmt (che formatta il codice sorgente Go) le utilizza. L'indentazione a due o quattro spazi è comune quando il JSON è destinato a un consumatore JavaScript o Python. Ecco la stessa struct con entrambi gli stili di indentazione affiancati:
// Indentazione con tabulazioni — preferita negli strumenti nativi Go
data, _ := json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }
// Indentazione a due spazi — comune per API con consumatori JS/Python
data, _ = json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }
// Indentazione a quattro spazi
data, _ = json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }json.Marshal(v) quando hai bisogno di output compatto — payload di rete, valori in cache o qualsiasi percorso dove la dimensione in byte conta. Accetta lo stesso argomento di valore e ha la stessa semantica degli errori, ma produce JSON su una singola riga senza spazi bianchi.Struct Tags — Controllare nomi di campo e omitempty
I struct tag di Go sono letterali di stringa posizionati dopo le dichiarazioni di campo che indicano a encoding/jsoncome serializzare ogni campo. Ci sono tre direttive chiave: json:"name" rinomina il campo nell'output, omitempty omette il campo quando contiene il valore zero del suo tipo, e json:"-" esclude il campo completamente — utile per password, identificatori interni o campi che non devono mai uscire dal confine del servizio.
type UserProfile struct {
ID string `json:"id"`
Email string `json:"email"`
DisplayName string `json:"display_name,omitempty"` // omettere se stringa vuota
AvatarURL *string `json:"avatar_url,omitempty"` // omettere se puntatore nil
IsAdmin bool `json:"is_admin,omitempty"` // omettere se false
passwordHash string // non esportato — escluso automaticamente
}
// Utente con tutti i campi opzionali popolati
full := UserProfile{
ID: "usr_7b3c", Email: "m.rossi@esempio.it",
DisplayName: "Marco Rossi", IsAdmin: true,
}
// {
// "id": "usr_7b3c",
// "email": "m.rossi@esempio.it",
// "display_name": "Marco Rossi",
// "is_admin": true
// }
// Utente senza campi opzionali — vengono omessi completamente
minimal := UserProfile{ID: "usr_2a91", Email: "s.bianchi@esempio.it"}
// {
// "id": "usr_2a91",
// "email": "s.bianchi@esempio.it"
// }Il tag json:"-" è la scelta giusta per i campi che devono essere esclusi incondizionatamente indipendentemente dal loro valore — tipicamente segreti, campi di tracciamento interno o dati corretti in memoria che non devono mai essere serializzati verso alcun sistema esterno.
type AuthToken struct {
TokenID string `json:"token_id"`
Subject string `json:"sub"`
IssuedAt int64 `json:"iat"`
ExpiresAt int64 `json:"exp"`
SigningKey []byte `json:"-"` // mai serializzato
RefreshToken string `json:"-"` // mai serializzato
}
tok := AuthToken{
TokenID: "tok_8f2a", Subject: "usr_7b3c",
IssuedAt: 1741614120, ExpiresAt: 1741617720,
SigningKey: []byte("segreto"), RefreshToken: "rt_9e4f",
}
data, _ := json.MarshalIndent(tok, "", " ")
// {
// "token_id": "tok_8f2a",
// "sub": "usr_7b3c",
// "iat": 1741614120,
// "exp": 1741617720
// }
// SigningKey e RefreshToken non appaiono maiencoding/json indipendentemente da qualsiasi tag. Non è necessario aggiungere json:"-"ai campi non esportati — l'esclusione è automatica e non può essere sovrascritta.MarshalJSON() personalizzato — Gestire tipi non standard
Qualsiasi tipo Go può implementare l'interfaccia json.Marshaler definendo un metodo MarshalJSON() ([]byte, error). Quando encoding/json incontra un tale tipo, chiama il metodo invece della sua serializzazione basata su reflection predefinita. Questo è il pattern Go canonico per i tipi di dominio che necessitano di una rappresentazione di rete specifica — valori monetari, enum di stato, formati di data personalizzati o qualsiasi tipo che memorizza i dati in modo diverso da come devono essere serializzati.
Tipo personalizzato — Money con conversione centesimi in decimale
package main
import (
"encoding/json"
"fmt"
"log"
)
type Money struct {
Amount int64 // memorizzato in centesimi per evitare drift in virgola mobile
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: "EUR"},
Tax: Money{Amount: 1592, Currency: "EUR"},
Total: Money{Amount: 21492, Currency: "EUR"},
}
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": "EUR", "display": "EUR 199.00" },
// "tax": { "amount": 15.92, "currency": "EUR", "display": "EUR 15.92" },
// "total": { "amount": 214.92, "currency": "EUR", "display": "EUR 214.92" }
// }Enum di stato — Rappresentazione come stringa
type OrderStatus int
const (
StatusPending OrderStatus = iota
StatusPaid
StatusShipped
StatusCancelled
)
var orderStatusNames = map[OrderStatus]string{
StatusPending: "in_attesa",
StatusPaid: "pagato",
StatusShipped: "spedito",
StatusCancelled: "annullato",
}
func (s OrderStatus) MarshalJSON() ([]byte, error) {
name, ok := orderStatusNames[s]
if !ok {
return nil, fmt.Errorf("stato ordine sconosciuto: %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": "spedito"
// }MarshalJSON() e UnmarshalJSON() insieme. Se implementi solo la serializzazione, fare un ciclo completo del tipo attraverso JSON (serializzare → memorizzare → deserializzare) perderà silenziosamente struttura o restituirà il tipo sbagliato. La coppia forma un contratto che garantisce che il tipo possa sopravvivere a un ciclo JSON completo.UUID — Serializzare come stringa
La libreria standard di Go non ha un tipo UUID. La scelta più comune è github.com/google/uuid, che implementa già MarshalJSON() e serializza come una stringa RFC 4122 tra virgolette. Se usi un [16]byte grezzo o un tipo ID personalizzato, implementa l'interfaccia tu stesso per evitare blob binari codificati in base64 nell'output JSON.
import (
"encoding/json"
"fmt"
"github.com/google/uuid"
)
type AuditEvent struct {
EventID uuid.UUID `json:"event_id"` // serializzato come "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: "utente.password_modificata",
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": "utente.password_modificata",
// "actor_id": "usr_7f3a91bc",
// "occurred_at": "2026-03-10T14:22:00Z"
// }[16]byte senza un tipo personalizzato, encoding/json li codifica come stringa base64 — es. "VQ6EAOKbQdSnFkRmVUQAAA==". Usa sempre un tipo UUID appropriato o implementa MarshalJSON() per emettere il formato canonico di stringa con trattini.Riferimento parametri di json.MarshalIndent()
La firma della funzione è func MarshalIndent(v any, prefix, indent string) ([]byte, error). Sia prefix che indent sono letterali di stringa — non esiste una scorciatoia numerica come l' indent=4 di Python.
Opzioni comuni di struct tag:
json.Indent() — Riformattare byte JSON esistenti
Quando hai già un []byte di JSON — ad esempio dal corpo di una risposta HTTP, una colonna jsonb Postgres, o un file letto con os.ReadFile — non hai bisogno di definire una struct e fare unmarshal prima di poter formattare. json.Indent riformatta direttamente i byte grezzi scrivendo l'output indentato in un bytes.Buffer.
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
)
func main() {
// Simulazione di un payload JSON grezzo da un servizio 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
// }Un pattern che uso frequentemente nei microservizi è chiamare json.Indent prima di scrivere nei log strutturati — aggiunge un overhead trascurabile e rende le voci di log molto più facili da leggere durante un incidente. La funzione è particolarmente utile per registrare risposte HTTP, formattare stringhe JSON memorizzate e pipeline di format-on-read dove la definizione della struct non è disponibile.
func logResponse(logger *slog.Logger, statusCode int, body []byte) {
var pretty bytes.Buffer
if err := json.Indent(&pretty, body, "", " "); err != nil {
// Il corpo non è JSON valido — registrare grezzo
logger.Debug("risposta upstream", "status", statusCode, "body", string(body))
return
}
logger.Debug("risposta upstream", "status", statusCode, "body", pretty.String())
}json.Indent NON valida completamente il JSON oltre a ciò che è strutturalmente necessario per inserire gli spazi bianchi. Per la validazione sintattica completa, chiama json.Valid(data)prima e gestisci il caso false prima di tentare l'indentazione.Formattare JSON da un file e da una risposta HTTP
Due degli scenari più comuni nei servizi Go sono la formattazione di JSON letto da un file su disco (file di configurazione, dati di fixture, seed di migrazione) e la formattazione di corpi di risposta HTTP per il logging di debug o le asserzioni nei test. Entrambi seguono lo stesso pattern: leggere i byte, chiamare json.Indent o fare unmarshal e poi json.MarshalIndent, riscrivere o registrare.
Leggere file → Formattare → Riscrivere
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("leggere %s: %w", path, err)
}
if !json.Valid(data) {
return fmt.Errorf("JSON non valido in %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("scrivere %s: %w", path, err)
}
return nil
}
func main() {
if err := formatJSONFile("config/database.json"); err != nil {
log.Fatalf("formattare config: %v", err)
}
fmt.Println("config/database.json formattato con successo")
}Risposta HTTP → Decodificare → Formattare per il logging di debug
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("decodificare risposta health: %v", err)
}
pretty, err := json.MarshalIndent(result, "", " ")
if err != nil {
log.Fatalf("marshal risposta 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 risposta grezzo → json.Indent (senza struct necessaria)
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("leggere corpo: %v", err)
}
var buf bytes.Buffer
if err := json.Indent(&buf, body, "", " "); err != nil {
log.Fatalf("indent: %v", err)
}
fmt.Println(buf.String())
}Visualizzare JSON formattato da una risposta HTTP in Go
I due approcci precedenti coprono i casi più comuni: decodificare in una struct tipizzata e poi chiamare json.MarshalIndent (meglio quando devi validare o ispezionare campi specifici), oppure leggere i byte grezzi del corpo con io.ReadAll e chiamare direttamente json.Indent (meglio per il logging di debug rapido quando non hai una definizione di struct a portata di mano). L'approccio con byte grezzi è più semplice ma non fornisce la sicurezza dei tipi Go né l'accesso ai campi — è puramente una trasformazione di visualizzazione. Entrambi gli approcci gestiscono correttamente corpi di risposta grandi finché il corpo completo rientra nella memoria.
// Pattern A: decodifica tipizzata → MarshalIndent // Usa quando devi ispezionare o validare campi specifici var result map[string]any json.NewDecoder(resp.Body).Decode(&result) pretty, _ := json.MarshalIndent(result, "", " ") fmt.Println(string(pretty)) // Pattern B: byte grezzi → json.Indent // Usa per logging di debug rapido — senza definizione di struct necessaria body, _ := io.ReadAll(resp.Body) var buf bytes.Buffer json.Indent(&buf, body, "", " ") fmt.Println(buf.String())
Formattazione JSON da riga di comando nei progetti Go
A volte hai bisogno di formattare un payload JSON direttamente nel terminale senza scrivere un programma Go. Questi comandi one-liner sono quelli che ho in memoria muscolare durante lo sviluppo e la risposta agli incidenti.
echo '{"service":"payments","port":8443,"workers":4}' | python3 -m json.tool
# {
# "service": "payments",
# "port": 8443,
# "workers": 4
# }# Solo formattare cat api-response.json | jq . # Estrarre un campo annidato cat api-response.json | jq '.checks.database' # Filtrare un array cat audit-log.json | jq '.[] | select(.severity == "error")'
# main.go: legge stdin, formatta, scrive 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 formatta il codice sorgente Go, non il JSON. Non inviare file JSON attraverso gofmt — produrrà un errore o un output irriconoscibile. Usa jq . o python3 -m json.tool per i file JSON.Alternativa ad alte prestazioni — go-json
Per la grande maggioranza dei servizi Go, encoding/json è abbastanza veloce. Ma se la serializzazione JSON appare nel tuo profiler — comune nelle API REST ad alto throughput o nei servizi che emettono grosse righe di log strutturate a ogni richiesta — la libreria go-json è un sostituto diretto 3–5× più veloce con una superficie API identica.
go get github.com/goccy/go-json
package main
import (
// Sostituisci questo:
// "encoding/json"
// Con questo — API identica, nessun'altra modifica al codice:
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: "fattura.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))
// L'output è identico a encoding/json — differisce solo la velocità
}github.com/bytedance/sonic è la libreria JSON più veloce disponibile per Go, ma funziona solo su amd64 e arm64 (usa la compilazione JIT). Usa go-json quando hai bisogno di un sostituto portabile; ricorri a sonic quando sei su un'architettura nota e hai bisogno di ogni microsecondo in un percorso critico.
Lavorare con file JSON grandi
Sia json.MarshalIndent che json.Indent richiedono che l'intero payload sia in memoria. Per file superiori a 100 MB — esportazioni di dati, log di audit, payload di consumer Kafka — usa json.Decoder per fare lo streaming dell'input e processare i record uno alla volta.
Streaming di un grande array JSON con 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("aprire %s: %w", path, err)
}
defer file.Close()
dec := json.NewDecoder(file)
// Leggere il '[' di apertura
if _, err := dec.Token(); err != nil {
return fmt.Errorf("leggere token di apertura: %w", err)
}
var processed int
for dec.More() {
var event AuditEvent
if err := dec.Decode(&event); err != nil {
return fmt.Errorf("decodificare evento %d: %w", processed, err)
}
// Processare un evento alla volta — utilizzo memoria costante
if event.Severity == "error" {
fmt.Printf("[ERROR] %s: %s (%dms)
", event.UserID, event.Action, event.DurationMs)
}
processed++
}
fmt.Printf("Processati %d eventi di audit
", processed)
return nil
}
func main() {
if err := processAuditLog("audit-2026-03.json"); err != nil {
log.Fatalf("processare log di audit: %v", err)
}
}NDJSON — Un oggetto JSON per riga
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("aprire log: %v", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024) // 1 MB per riga
for scanner.Scan() {
var line LogLine
if err := json.Unmarshal(scanner.Bytes(), &line); err != nil {
continue // saltare le righe malformate
}
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.ReadFilealloca quell'intero buffer sull'heap, genera pressione GC e può causare OOM nei container con memoria limitata.Errori comuni
Problema: Scartare l'errore con _ significa che un valore non serializzabile (un canale, una funzione, un numero complesso) produce silenziosamente nil come output o causa un panic più avanti quando chiami string(nil).
Soluzione: Controlla sempre l'errore. Se stai serializzando un tipo che dovrebbe sempre avere successo, un log.Fatalf con panic è meglio della perdita silenziosa di dati.
data, _ := json.MarshalIndent(payload, "", " ") fmt.Println(string(data)) // stringa vuota se il marshal è fallito
data, err := json.MarshalIndent(payload, "", " ")
if err != nil {
log.Fatalf("marshal payload: %v", err)
}
fmt.Println(string(data))Problema: fmt.Println(string(data)) aggiunge un carattere di nuova riga dopo il JSON, il che corrompe le pipeline che trattano l'output come byte grezzi — ad esempio quando si invia a jq o si scrive in un protocollo binario.
Soluzione: Usa os.Stdout.Write(data) per output binario pulito. Se hai bisogno di una nuova riga finale per la visualizzazione umana, aggiungila esplicitamente.
data, _ := json.MarshalIndent(cfg, "", " ") fmt.Println(string(data)) // aggiunge una nuova riga extra alla fine
data, _ := json.MarshalIndent(cfg, "", " ")
os.Stdout.Write(data)
os.Stdout.Write([]byte("
")) // nuova riga esplicita solo quando necessarioProblema: Senza omitempty, un puntatore nil *string o *int viene serializzato come "field": null. Questo espone nomi di campi interni e può rompere i validatori di schema JSON rigidi sul lato del consumatore.
Soluzione: Aggiungi omitempty ai campi puntatore che vuoi assenti (non null) nell'output. Un *T nil con omitempty non produce alcuna chiave nel JSON.
type WebhookPayload struct {
EventID string `json:"event_id"`
ErrorMsg *string `json:"error_msg"` // appare come null quando nil
}
// {"event_id":"evt_3c7f","error_msg":null}type WebhookPayload struct {
EventID string `json:"event_id"`
ErrorMsg *string `json:"error_msg,omitempty"` // omesso quando nil
}
// {"event_id":"evt_3c7f"}Problema: Fare unmarshal in map[string]any perde le informazioni sui tipi, richiede type assertion manuali e produce un ordine di chiavi non deterministico — rendendo i diff JSON e i confronti di log più difficili.
Soluzione: Definisci una struct con i json tag appropriati. Le struct sono type-safe, serializzano più velocemente, producono un ordine di campi deterministico corrispondente alla definizione della struct e rendono il codice auto-documentante.
var result map[string]any json.Unmarshal(body, &result) port := result["port"].(float64) // type assertion necessaria, causa panic se tipo sbagliato
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 // tipizzato, sicuro, veloceencoding/json vs alternative — Confronto rapido
Usa json.MarshalIndent per qualsiasi caso in cui controlli la definizione della struct e hai bisogno di output formattato — file di configurazione, logging di debug, fixture di test e logging delle risposte API. Usa json.Indent quando hai già byte grezzi e hai solo bisogno di aggiungere spazi bianchi senza andata e ritorno attraverso i tipi Go. Passa a go-json o sonic solo dopo che il profiling conferma che la serializzazione JSON è un collo di bottiglia misurabile — per la maggior parte dei servizi, la libreria standard è più che sufficiente.
Domande frequenti
Come visualizzo JSON formattato in Go?
Chiama json.MarshalIndent(v, "", "\t") dal package encoding/json — il secondo argomento è un prefisso per riga (di solito vuoto) e il terzo è l'indentazione per livello. Passa "\t" per le tabulazioni o " " per due spazi. Non è necessaria alcuna libreria esterna; encoding/json è incluso nella libreria standard di Go.
package main
import (
"encoding/json"
"fmt"
"log"
)
func main() {
config := map[string]any{
"service": "payments-api",
"port": 8443,
"region": "eu-south-1",
}
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
log.Fatalf("marshal: %v", err)
}
fmt.Println(string(data))
// {
// "port": 8443,
// "region": "eu-south-1",
// "service": "payments-api"
// }
}Qual è la differenza tra json.Marshal e json.MarshalIndent?
json.Marshal produce JSON compatto su una singola riga senza spazi bianchi — ideale per il trasferimento di rete dove ogni byte conta. json.MarshalIndent accetta due parametri extra (prefix e indent) e produce un output indentato e leggibile. Entrambe le funzioni accettano gli stessi tipi di valore e restituiscono ([]byte, error). L'unico costo di MarshalIndent è qualche byte in più nell'output e una quantità trascurabile di CPU extra per inserire gli spazi.
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
// }Come formatto un []byte JSON senza fare unmarshal della struct?
Usa json.Indent(&buf, src, "", "\t"). Questa funzione prende un []byte JSON esistente e scrive la versione indentata in un bytes.Buffer — senza definire una struct, senza type assertion, senza andata e ritorno attraverso i tipi Go. È l'opzione più rapida quando hai già byte JSON grezzi, ad esempio dal corpo di una risposta HTTP o da una colonna di database.
import (
"bytes"
"encoding/json"
"fmt"
"log"
)
raw := []byte(`{"endpoint":"/api/v2/fatture","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/fatture",
// "page": 1,
// "per_page": 50,
// "total": 312
// }Perché json.MarshalIndent restituisce un errore?
encoding/json restituisce un errore quando il valore non può essere rappresentato come JSON. Le cause più comuni sono: serializzare un canale, una funzione o un numero complesso (questi tipi non hanno equivalente JSON); una struct che implementa MarshalJSON() e restituisce un errore; o una map con chiavi che non sono string. Importante: serializzare una struct con un campo non esportato o un puntatore nil NON causa un errore — vengono semplicemente omessi.
import (
"encoding/json"
"fmt"
)
// Questo restituirà un errore — i canali non sono serializzabili come JSON
ch := make(chan int)
_, err := json.MarshalIndent(ch, "", " ")
fmt.Println(err)
// json: unsupported type: chan int
// Questo è corretto — i campi puntatore nil con omitempty vengono omessi silenziosamente
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 omesso, nessun erroreCome escludo un campo dall'output JSON in Go?
Esistono tre modi. Primo, usa il struct tag json:"-" — il campo è sempre escluso indipendentemente dal suo valore. Secondo, usa omitempty — il campo è escluso solo quando contiene il valore zero del suo tipo (puntatore nil, stringa vuota, 0, false). Terzo, i campi non esportati (in minuscolo) sono esclusi automaticamente da encoding/json senza bisogno di alcun 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 escluso
BillingName string `json:"billing_name,omitempty"` // escluso se vuoto
internalRef string // non esportato — escluso automaticamente
}
pm := PaymentMethod{
ID: "pm_9f3a", Last4: "4242",
ExpiryMonth: 12, ExpiryYear: 2028,
CVV: "123", internalRef: "stripe:pm_9f3a",
}
data, _ := json.MarshalIndent(pm, "", " ")
// CVV, BillingName (vuoto) e internalRef non appaiono nell'outputCome gestisco time.Time nella serializzazione JSON?
encoding/json serializza time.Time nel formato RFC3339Nano per impostazione predefinita (es. "2026-03-10T14:22:00Z"), compatibile con ISO 8601. Se hai bisogno di un formato diverso — come interi Unix epoch per una API legacy, o una stringa solo data — implementa MarshalJSON() su un tipo wrapper che incorpora time.Time e restituisce il formato di cui hai bisogno.
import (
"encoding/json"
"fmt"
"time"
)
// Comportamento predefinito — RFC3339Nano, senza codice personalizzato
type AuditEvent struct {
Action string `json:"action"`
OccurredAt time.Time `json:"occurred_at"`
}
e := AuditEvent{
Action: "fattura.pagata",
OccurredAt: time.Date(2026, 3, 10, 14, 22, 0, 0, time.UTC),
}
data, _ := json.MarshalIndent(e, "", " ")
// {
// "action": "fattura.pagata",
// "occurred_at": "2026-03-10T14:22:00Z"
// }
// Personalizzato: timestamp Unix come intero
type UnixTime struct{ time.Time }
func (u UnixTime) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("%d", u.Unix())), nil
}Strumenti correlati
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.