JSON Formatter Go — Guide MarshalIndent()

·Systems Engineer·Révisé parTobias Müller·Publié

Utilisez le Formateur et Embellisseur JSON gratuit directement dans votre navigateur — sans installation.

Essayer Formateur et Embellisseur JSON en ligne →

Quand je travaille sur un microservice Go et que j'ai besoin d'inspecter une réponse d'API ou un fichier de configuration, le JSON compact est le premier obstacle — une seule ligne avec des centaines de champs imbriqués ne me dit presque rien d'un coup d'œil. Pour formater du JSON en Go, la bibliothèque standard vous donne tout ce qu'il faut : json.MarshalIndent est intégré dans encoding/json, livré avec chaque installation de Go et ne requiert aucune dépendance tierce. Ce guide couvre le tableau complet : struct tags, implémentations personnalisées de MarshalJSON() , json.Indent pour reformater des bytes bruts, streaming de gros fichiers avec json.Decoder, et quand recourir à go-json pour les chemins à haut débit, ainsi que des commandes en une ligne pour formater rapidement dans le terminal. Tous les exemples utilisent Go 1.21+.

  • json.MarshalIndent(v, "", "\t") appartient à la bibliothèque standard — zéro dépendance, inclus dans chaque installation Go.
  • Les struct tags json:"field_name,omitempty" contrôlent les clés de sérialisation et omettent les champs à valeur zéro.
  • Implémentez MarshalJSON() sur n'importe quel type pour contrôler entièrement sa représentation JSON.
  • json.Indent() reformate des []byte déjà sérialisés sans re-parser la struct — plus rapide pour les bytes bruts.
  • Pour les gros fichiers (>100 Mo) : utilisez json.Decoder avec Token() pour streamer sans tout charger en mémoire.
  • go-json est un remplacement direct 3–5× plus rapide que encoding/json pour les APIs à fort débit.

Qu'est-ce que le formatage JSON ?

Le formatage JSON — aussi appelé pretty-printing — transforme une chaîne JSON compacte et minifiée en une mise en page lisible avec une indentation et des sauts de ligne cohérents. Les données sous-jacentes sont identiques ; seuls les espaces blancs changent. Le JSON compact est optimal pour le transfert réseau où chaque octet compte ; le JSON formaté est optimal pour le débogage, la revue de code, l'inspection de logs et la rédaction de fichiers de configuration. Le package encoding/json de Go gère les deux modes avec un seul appel de fonction — alternez entre sortie compacte et indentée en choisissant entre json.Marshal et json.MarshalIndent.

Before · json
After · json
{"service":"payments","port":8443,"workers":4}
{
	"service": "payments",
	"port": 8443,
	"workers": 4
}

json.MarshalIndent() — L'approche de la bibliothèque standard

json.MarshalIndent se trouve dans le package encoding/json , qui fait partie de la bibliothèque standard Go — aucun go get requis. Sa signature est MarshalIndent(v any, prefix, indent string) ([]byte, error) : la chaîne prefix est préfixée à chaque ligne de sortie (presque toujours laissée vide), et indent est répétée une fois par niveau d'imbrication. Passez "\t" pour les tabulations ou " " pour deux espaces.

Go — exemple minimal fonctionnel
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"
// 	]
// }

Le choix entre tabulations et espaces est essentiellement une convention d'équipe. Beaucoup de projets Go préfèrent les tabulations car gofmt (qui formate le code source Go) les utilise. L'indentation à deux ou quatre espaces est courante quand le JSON est destiné à un consommateur JavaScript ou Python. Voici la même struct avec les deux styles d'indentation côte à côte :

Go — tabulations vs espaces
// Indentation avec tabulations — préférée dans les outils natifs Go
data, _ := json.MarshalIndent(cfg, "", "	")
// {
// 	"host": "payments-api.internal",
// 	"port": 8443
// }

// Indentation à deux espaces — courante pour les APIs avec consommateurs JS/Python
data, _ = json.MarshalIndent(cfg, "", "  ")
// {
//   "host": "payments-api.internal",
//   "port": 8443
// }

// Indentation à quatre espaces
data, _ = json.MarshalIndent(cfg, "", "    ")
// {
//     "host": "payments-api.internal",
//     "port": 8443
// }
Note :Utilisez json.Marshal(v) quand vous avez besoin d'une sortie compacte — payloads réseau, valeurs en cache ou tout chemin où la taille en octets importe. Il accepte le même argument de valeur et a la même sémantique d'erreur, mais produit du JSON sur une seule ligne sans espaces blancs.

Struct Tags — Contrôler les noms de champs et omitempty

Les struct tags Go sont des littéraux de chaîne placés après les déclarations de champ qui indiquent à encoding/jsoncomment sérialiser chaque champ. Il existe trois directives clés : json:"name" renomme le champ dans la sortie, omitempty omet le champ quand il contient la valeur zéro de son type, et json:"-" exclut le champ entièrement — utile pour les mots de passe, les identifiants internes ou les champs qui ne doivent jamais quitter la frontière du service.

Go — struct tags pour une réponse d'API
type UserProfile struct {
	ID          string  `json:"id"`
	Email       string  `json:"email"`
	DisplayName string  `json:"display_name,omitempty"`  // omettre si string vide
	AvatarURL   *string `json:"avatar_url,omitempty"`    // omettre si pointeur nil
	IsAdmin     bool    `json:"is_admin,omitempty"`      // omettre si false
	passwordHash string                                   // non exporté — exclu automatiquement
}

// Utilisateur avec tous les champs optionnels renseignés
full := UserProfile{
	ID: "usr_7b3c", Email: "t.dupont@exemple.fr",
	DisplayName: "Thomas Dupont", IsAdmin: true,
}
// {
//   "id": "usr_7b3c",
//   "email": "t.dupont@exemple.fr",
//   "display_name": "Thomas Dupont",
//   "is_admin": true
// }

// Utilisateur sans champs optionnels — ils sont entièrement omis
minimal := UserProfile{ID: "usr_2a91", Email: "c.martin@exemple.fr"}
// {
//   "id": "usr_2a91",
//   "email": "c.martin@exemple.fr"
// }

Le tag json:"-" est le bon choix pour les champs qui doivent être inconditionnellement exclus quelle que soit leur valeur — typiquement les secrets, les champs de suivi internes, ou les données correctes en mémoire qui ne doivent jamais être sérialisées vers un système externe.

Go — exclure les champs sensibles
type AuthToken struct {
	TokenID      string `json:"token_id"`
	Subject      string `json:"sub"`
	IssuedAt     int64  `json:"iat"`
	ExpiresAt    int64  `json:"exp"`
	SigningKey    []byte `json:"-"`   // jamais sérialisé
	RefreshToken string `json:"-"`   // jamais sérialisé
}

tok := AuthToken{
	TokenID: "tok_8f2a", Subject: "usr_7b3c",
	IssuedAt: 1741614120, ExpiresAt: 1741617720,
	SigningKey: []byte("secret"), RefreshToken: "rt_9e4f",
}
data, _ := json.MarshalIndent(tok, "", "  ")
// {
//   "token_id": "tok_8f2a",
//   "sub": "usr_7b3c",
//   "iat": 1741614120,
//   "exp": 1741617720
// }
// SigningKey et RefreshToken n'apparaissent jamais
Note :Les champs de struct non exportés (en minuscule) sont toujours exclus par encoding/jsonindépendamment de tout tag. Vous n'avez pas besoin d'ajouter json:"-"aux champs non exportés — l'exclusion est automatique et ne peut pas être annulée.

MarshalJSON() personnalisé — Gérer les types non standard

N'importe quel type Go peut implémenter l'interface json.Marshaler en définissant une méthode MarshalJSON() ([]byte, error). Quand encoding/json rencontre un tel type, il appelle la méthode au lieu de sa sérialisation par réflexion par défaut. C'est le pattern Go canonique pour les types de domaine qui ont besoin d'une représentation réseau spécifique — valeurs monétaires, enums de statut, formats de date personnalisés, ou tout type qui stocke les données différemment de la façon dont elles doivent être sérialisées.

Type personnalisé — Money avec conversion centimes vers décimal

Go — MarshalJSON personnalisé pour Money
package main

import (
	"encoding/json"
	"fmt"
	"log"
)

type Money struct {
	Amount   int64  // stocké en centimes pour éviter la dérive virgule flottante
	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 de statut — Représentation sous forme de chaîne

Go — MarshalJSON personnalisé pour enum de statut
type OrderStatus int

const (
	StatusPending OrderStatus = iota
	StatusPaid
	StatusShipped
	StatusCancelled
)

var orderStatusNames = map[OrderStatus]string{
	StatusPending:   "en_attente",
	StatusPaid:      "payé",
	StatusShipped:   "expédié",
	StatusCancelled: "annulé",
}

func (s OrderStatus) MarshalJSON() ([]byte, error) {
	name, ok := orderStatusNames[s]
	if !ok {
		return nil, fmt.Errorf("statut de commande inconnu : %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": "expédié"
// }
Note :Implémentez toujours MarshalJSON() et UnmarshalJSON()ensemble. Si vous n'implémentez que la sérialisation, faire un aller-retour du type par JSON (sérialiser → stocker → désérialiser) perdra silencieusement de la structure ou retournera le mauvais type. La paire forme un contrat garantissant que le type peut survivre un aller-retour JSON complet.

UUID — Sérialiser comme chaîne

La bibliothèque standard Go n'a pas de type UUID. Le choix le plus courant est github.com/google/uuid, qui implémente déjà MarshalJSON() et sérialise comme une chaîne RFC 4122 entre guillemets. Si vous utilisez un [16]byte brut ou un type ID personnalisé, implémentez l'interface vous-même pour éviter les blobs binaires encodés en base64 dans votre sortie JSON.

Go 1.21+ — sérialisation UUID
import (
    "encoding/json"
    "fmt"

    "github.com/google/uuid"
)

type AuditEvent struct {
    EventID   uuid.UUID `json:"event_id"`   // sérialisé comme "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:    "utilisateur.mot_de_passe_modifié",
    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": "utilisateur.mot_de_passe_modifié",
//   "actor_id": "usr_7f3a91bc",
//   "occurred_at": "2026-03-10T14:22:00Z"
// }
Note :Si vous stockez des UUIDs en [16]byte sans type personnalisé, encoding/json les encode en chaîne base64 — ex. : "VQ6EAOKbQdSnFkRmVUQAAA==". Utilisez toujours un type UUID approprié ou implémentez MarshalJSON() pour émettre le format canonique de chaîne avec tirets.

Référence des paramètres de json.MarshalIndent()

La signature de la fonction est func MarshalIndent(v any, prefix, indent string) ([]byte, error). Aussi bien prefix qu' indent sont des littéraux de chaîne — il n'y a pas de raccourci numérique comme le indent=4 de Python.

Paramètre
Type
Description
v
any
La valeur à sérialiser — struct, map, slice ou primitif
prefix
string
Chaîne préfixant chaque ligne de sortie (généralement "")
indent
string
Chaîne utilisée par niveau d'indentation ("\t" ou " ")

Options courantes de struct tags :

Tag
Effet
json:"name"
Renomme le champ en name dans la sortie JSON
json:"name,omitempty"
Renomme + omet si valeur zéro (nil, "", 0, false)
json:"-"
Exclut toujours ce champ de la sortie JSON
json:",string"
Encode un nombre ou un bool comme valeur string JSON

json.Indent() — Reformater des bytes JSON existants

Quand vous avez déjà un []byte de JSON — disons du corps d'une réponse HTTP, d'une colonne jsonb Postgres, ou d'un fichier lu avec os.ReadFile— vous n'avez pas besoin de définir une struct et de désérialiser avant de pouvoir formatter. json.Indent reformate directement les bytes bruts en écrivant la sortie indentée dans un bytes.Buffer.

Go — json.Indent sur des bytes bruts
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
)

func main() {
	// Simulation d'un payload JSON brut d'un service 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 que j'utilise régulièrement dans les microservices est d'appeler json.Indent avant d'écrire dans les logs structurés — cela ajoute un overhead négligeable et rend les entrées de log beaucoup plus faciles à lire lors d'un incident. La fonction est particulièrement utile pour journaliser les réponses HTTP, formatter les chaînes JSON stockées et les pipelines de format-à-la-lecture où la définition de la struct n'est pas disponible.

Go — json.Indent pour le logging de débogage
func logResponse(logger *slog.Logger, statusCode int, body []byte) {
	var pretty bytes.Buffer
	if err := json.Indent(&pretty, body, "", "  "); err != nil {
		// Le corps n'est pas du JSON valide — journaliser brut
		logger.Debug("réponse upstream", "status", statusCode, "body", string(body))
		return
	}
	logger.Debug("réponse upstream", "status", statusCode, "body", pretty.String())
}
Attention :json.Indent NE valide PAS complètement le JSON au-delà de ce qui est structurellement nécessaire pour insérer les espaces blancs. Pour une validation syntaxique complète, appelez json.Valid(data)d'abord et gérez le cas false avant de tenter l'indentation.

Formater du JSON depuis un fichier et une réponse HTTP

Deux des scénarios les plus courants dans les services Go sont le formatage de JSON lu depuis un fichier sur disque (fichiers de configuration, données de fixtures, seeds de migration) et la mise en forme de corps de réponses HTTP pour le logging de débogage ou les assertions de tests. Les deux suivent le même pattern : lire les bytes, appeler json.Indent ou désérialiser puis json.MarshalIndent, réécrire ou journaliser.

Lire un fichier → Formater → Réécrire

Go — formater un fichier JSON sur place
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("lire %s : %w", path, err)
	}

	if !json.Valid(data) {
		return fmt.Errorf("JSON invalide dans %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("écrire %s : %w", path, err)
	}
	return nil
}

func main() {
	if err := formatJSONFile("config/database.json"); err != nil {
		log.Fatalf("formater config : %v", err)
	}
	fmt.Println("config/database.json formaté avec succès")
}

Réponse HTTP → Décoder → Formater pour le logging de débogage

Go — formater une réponse HTTP pour le log de débogage
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("décoder réponse health : %v", err)
	}

	pretty, err := json.MarshalIndent(result, "", "  ")
	if err != nil {
		log.Fatalf("marshal réponse 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
// }

Corps de réponse brut → json.Indent (sans struct nécessaire)

Go — formater un corps HTTP brut avec io.ReadAll
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("lire corps : %v", err)
	}

	var buf bytes.Buffer
	if err := json.Indent(&buf, body, "", "  "); err != nil {
		log.Fatalf("indent : %v", err)
	}
	fmt.Println(buf.String())
}

Afficher du JSON formaté depuis une réponse HTTP en Go

Les deux approches ci-dessus couvrent les cas les plus courants : décoder dans une struct typée puis appeler json.MarshalIndent (mieux quand vous devez valider ou inspecter des champs spécifiques), ou lire les bytes bruts du corps avec io.ReadAll et appeler directement json.Indent (mieux pour le logging de débogage rapide quand vous n'avez pas de définition de struct sous la main). L'approche bytes bruts est plus simple mais ne vous donne pas la sécurité des types Go ni l'accès aux champs — c'est une transformation d'affichage pure. Les deux approches gèrent correctement les gros corps de réponse tant que le corps complet tient en mémoire.

Go — deux patterns côte à côte
// Pattern A : décodage typé → MarshalIndent
// Utiliser quand vous devez inspecter ou valider des champs spécifiques
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
pretty, _ := json.MarshalIndent(result, "", "  ")
fmt.Println(string(pretty))

// Pattern B : bytes bruts → json.Indent
// Utiliser pour le logging de débogage rapide — sans définition de struct nécessaire
body, _ := io.ReadAll(resp.Body)
var buf bytes.Buffer
json.Indent(&buf, body, "", "  ")
fmt.Println(buf.String())

Formatage JSON en ligne de commande dans les projets Go

Parfois vous avez besoin de formater un payload JSON directement dans le terminal sans écrire un programme Go. Ces commandes en une ligne sont celles que j'ai en mémoire musculaire pendant le développement et la réponse aux incidents.

bash — passer du JSON au formateur intégré de Python
echo '{"service":"payments","port":8443,"workers":4}' | python3 -m json.tool
# {
#     "service": "payments",
#     "port": 8443,
#     "workers": 4
# }
bash — passer à jq pour un formatage et filtrage complets
# Formater uniquement
cat api-response.json | jq .

# Extraire un champ imbriqué
cat api-response.json | jq '.checks.database'

# Filtrer un tableau
cat audit-log.json | jq '.[] | select(.severity == "error")'
bash — passer à un main.go minimal via stdin
# main.go : lit stdin, formate, écrit 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.go
Note :gofmt formate le code source Go, pas le JSON. Ne passez pas des fichiers JSON dans gofmt — il produira soit une erreur soit une sortie méconnaissable. Utilisez jq . ou python3 -m json.tool pour les fichiers JSON.

Alternative haute performance — go-json

Pour la grande majorité des services Go, encoding/json est suffisamment rapide. Mais si la sérialisation JSON apparaît dans votre profiler — courant dans les APIs REST à fort débit ou les services qui émettent de grosses lignes de log structurées à chaque requête — la bibliothèque go-json est un remplacement direct 3–5× plus rapide avec une surface d'API identique.

bash — installer go-json
go get github.com/goccy/go-json
Go — remplacer encoding/json par go-json avec un seul changement d'import
package main

import (
	// Remplacez ceci :
	// "encoding/json"

	// Par ceci — API identique, aucun autre changement de code :
	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: "facture.téléchargement", 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))
	// La sortie est identique à encoding/json — seule la vitesse diffère
}

github.com/bytedance/sonic est la bibliothèque JSON la plus rapide disponible pour Go, mais elle ne fonctionne que sur amd64 et arm64 (elle utilise la compilation JIT). Utilisez go-json quand vous avez besoin d'un remplacement portable ; recourez à sonic quand vous êtes sur une architecture connue et avez besoin de chaque microseconde dans un chemin critique.

Travailler avec de gros fichiers JSON

json.MarshalIndent et json.Indent requièrent tous deux que le payload complet soit en mémoire. Pour les fichiers au-dessus de 100 Mo — exports de données, logs d'audit, payloads de consommateurs Kafka — utilisez json.Decoder pour streamer l'entrée et traiter les enregistrements un par un.

Streaming d'un grand tableau JSON avec json.Decoder

Go — stream d'un grand tableau JSON sans charger en mémoire
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("ouvrir %s : %w", path, err)
	}
	defer file.Close()

	dec := json.NewDecoder(file)

	// Lire le '[' d'ouverture
	if _, err := dec.Token(); err != nil {
		return fmt.Errorf("lire token d'ouverture : %w", err)
	}

	var processed int
	for dec.More() {
		var event AuditEvent
		if err := dec.Decode(&event); err != nil {
			return fmt.Errorf("décoder événement %d : %w", processed, err)
		}
		// Traiter un événement à la fois — utilisation mémoire constante
		if event.Severity == "error" {
			fmt.Printf("[ERROR] %s: %s (%dms)
", event.UserID, event.Action, event.DurationMs)
		}
		processed++
	}
	fmt.Printf("Traité %d événements d'audit
", processed)
	return nil
}

func main() {
	if err := processAuditLog("audit-2026-03.json"); err != nil {
		log.Fatalf("traiter log d'audit : %v", err)
	}
}

NDJSON — Un objet JSON par ligne

Go — traiter un stream NDJSON ligne par ligne
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("ouvrir log : %v", err)
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	scanner.Buffer(make([]byte, 1024*1024), 1024*1024) // 1 Mo par ligne

	for scanner.Scan() {
		var line LogLine
		if err := json.Unmarshal(scanner.Bytes(), &line); err != nil {
			continue // ignorer les lignes mal formées
		}
		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)
	}
}
Note :Passez au streaming quand le fichier dépasse 100 Mo ou lors du traitement de streams non bornés (consommateurs Kafka, pipelines de logs, lectures d'objets S3). Charger un fichier JSON de 500 Mo avec os.ReadFile allouera tout ce buffer sur le tas, générera de la pression GC et pourra provoquer un OOM sur des conteneurs contraints en mémoire.

Erreurs fréquentes

Ignorer le retour d'erreur de MarshalIndent

Problème : Ignorer l'erreur avec _ signifie qu'une valeur non sérialisable (un canal, une fonction, un nombre complexe) produit silencieusement nil en sortie ou provoque un panic plus loin lors de l'appel à string(nil).

Solution : Vérifiez toujours l'erreur. Si vous sérialisez un type qui devrait toujours réussir, un log.Fatalf avec panic est préférable à une perte silencieuse de données.

Before · Go
After · Go
data, _ := json.MarshalIndent(payload, "", "  ")
fmt.Println(string(data)) // chaîne vide si le marshal a échoué
data, err := json.MarshalIndent(payload, "", "  ")
if err != nil {
    log.Fatalf("marshal payload : %v", err)
}
fmt.Println(string(data))
Utiliser fmt.Println au lieu de os.Stdout.Write pour une sortie binaire

Problème : fmt.Println(string(data)) ajoute un caractère de nouvelle ligne après le JSON, ce qui corrompt les pipelines qui traitent la sortie comme des bytes bruts — par exemple lors du passage à jq ou de l'écriture dans un protocole binaire.

Solution : Utilisez os.Stdout.Write(data) pour une sortie binaire propre. Si vous avez besoin d'une nouvelle ligne pour l'affichage humain, ajoutez-la explicitement.

Before · Go
After · Go
data, _ := json.MarshalIndent(cfg, "", "  ")
fmt.Println(string(data)) // ajoute une nouvelle ligne supplémentaire à la fin
data, _ := json.MarshalIndent(cfg, "", "  ")
os.Stdout.Write(data)
os.Stdout.Write([]byte("
")) // nouvelle ligne explicite seulement si nécessaire
Oublier omitempty sur les champs pointeur

Problème : Sans omitempty, un pointeur nil *string ou *int est sérialisé comme "field": null. Cela expose des noms de champs internes et peut casser les validateurs de schéma JSON stricts côté consommateur.

Solution : Ajoutez omitempty aux champs pointeur que vous voulez absents (pas null) dans la sortie. Un *T nil avec omitempty ne produit aucune clé dans le JSON.

Before · Go
After · Go
type WebhookPayload struct {
    EventID   string  `json:"event_id"`
    ErrorMsg  *string `json:"error_msg"`  // apparaît comme null si nil
}
// {"event_id":"evt_3c7f","error_msg":null}
type WebhookPayload struct {
    EventID   string  `json:"event_id"`
    ErrorMsg  *string `json:"error_msg,omitempty"`  // omis si nil
}
// {"event_id":"evt_3c7f"}
Utiliser map[string]interface{} au lieu de structs

Problème : Désérialiser dans map[string]any perd les informations de types, nécessite des type assertions manuelles et produit un ordre de clés non déterministe — rendant les diffs JSON et les comparaisons de logs plus difficiles.

Solution : Définissez une struct avec des json tags appropriés. Les structs sont type-safe, sérialisent plus vite, produisent un ordre de champs déterministe correspondant à la définition de la struct et rendent le code auto-documenté.

Before · Go
After · Go
var result map[string]any
json.Unmarshal(body, &result)
port := result["port"].(float64) // type assertion requise, panic si mauvais type
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 // typé, sûr, rapide

encoding/json vs alternatives — Comparaison rapide

Méthode
Sortie formatée
JSON valide
Types personnalisés
Streaming
Installation requise
json.MarshalIndent
✓ via MarshalJSON
Non (stdlib)
json.Indent
N/A (bytes seulement)
Non (stdlib)
json.Encoder
✗ (compact)
✓ via MarshalJSON
Non (stdlib)
go-json
go get
sonic
go get (amd64/arm64)
jq (CLI)
N/A
Installation système

Utilisez json.MarshalIndent pour tout cas où vous contrôlez la définition de la struct et avez besoin d'une sortie formatée — fichiers de configuration, logging de débogage, fixtures de tests et journalisation des réponses d'API. Utilisez json.Indent quand vous avez déjà des bytes bruts et avez juste besoin d'ajouter des espaces blancs sans aller-retour par les types Go. Passez à go-json ou sonic uniquement après que le profilage confirme que la sérialisation JSON est un goulot d'étranglement mesurable — pour la plupart des services, la bibliothèque standard est largement suffisante.

Questions fréquentes

Comment afficher du JSON formaté en Go ?

Appelez json.MarshalIndent(v, "", "\t") du package encoding/json — le deuxième argument est un préfixe par ligne (généralement vide) et le troisième est l'indentation par niveau. Passez "\t" pour les tabulations ou " " pour deux espaces. Aucune bibliothèque externe n'est nécessaire ; encoding/json est inclus dans la bibliothèque standard de Go.

Go
package main

import (
	"encoding/json"
	"fmt"
	"log"
)

func main() {
	config := map[string]any{
		"service": "payments-api",
		"port":    8443,
		"region":  "eu-west-3",
	}
	data, err := json.MarshalIndent(config, "", "	")
	if err != nil {
		log.Fatalf("marshal: %v", err)
	}
	fmt.Println(string(data))
	// {
	// 	"port": 8443,
	// 	"region": "eu-west-3",
	// 	"service": "payments-api"
	// }
}

Quelle est la différence entre json.Marshal et json.MarshalIndent ?

json.Marshal produit du JSON compact sur une seule ligne sans espace — idéal pour le transfert réseau où chaque octet compte. json.MarshalIndent accepte deux paramètres supplémentaires (prefix et indent) et produit une sortie indentée et lisible. Les deux fonctions acceptent les mêmes types de valeurs et retournent ([]byte, error). Le seul coût de MarshalIndent est quelques octets supplémentaires en sortie et un minimum de CPU pour insérer les espaces.

Go
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
// }

Comment formater un []byte JSON sans unmarshaler la struct ?

Utilisez json.Indent(&buf, src, "", "\t"). Cette fonction prend un []byte JSON existant et écrit la version indentée dans un bytes.Buffer — sans définir de struct, sans type assertion, sans aller-retour par les types Go. C'est l'option la plus rapide quand vous avez déjà des bytes JSON bruts, par exemple du corps d'une réponse HTTP ou d'une colonne de base de données.

Go
import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
)

raw := []byte(`{"endpoint":"/api/v2/factures","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/factures",
// 	"page": 1,
// 	"per_page": 50,
// 	"total": 312
// }

Pourquoi json.MarshalIndent retourne-t-il une erreur ?

encoding/json retourne une erreur quand la valeur ne peut pas être représentée en JSON. Les causes les plus fréquentes sont : sérialiser un canal, une fonction ou un nombre complexe (ces types n'ont pas d'équivalent JSON) ; une struct qui implémente MarshalJSON() et retourne une erreur ; ou une map avec des clés qui ne sont pas des strings. Sérialiser une struct avec un champ non exporté ou un pointeur nil ne cause PAS d'erreur — ils sont simplement omis.

Go
import (
	"encoding/json"
	"fmt"
)

// Cela retournera une erreur — les canaux ne sont pas sérialisables en JSON
ch := make(chan int)
_, err := json.MarshalIndent(ch, "", "	")
fmt.Println(err)
// json: unsupported type: chan int

// Cela est correct — les champs pointeur nil avec omitempty sont omis silencieusement
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 omis, sans erreur

Comment exclure un champ de la sortie JSON en Go ?

Il existe trois façons. Premièrement, utilisez le struct tag json:"-" — le champ est toujours exclu quelle que soit sa valeur. Deuxièmement, utilisez omitempty — le champ est exclu uniquement quand il contient la valeur zéro de son type (pointeur nil, string vide, 0, false). Troisièmement, les champs non exportés (en minuscule) sont automatiquement exclus par encoding/json sans aucun tag nécessaire.

Go
type PaymentMethod struct {
	ID           string  `json:"id"`
	Last4        string  `json:"last4"`
	ExpiryMonth  int     `json:"expiry_month"`
	ExpiryYear   int     `json:"expiry_year"`
	CVV          string  `json:"-"`                    // toujours exclu
	BillingName  string  `json:"billing_name,omitempty"` // exclu si vide
	internalRef  string                                 // non exporté — exclu automatiquement
}

pm := PaymentMethod{
	ID: "pm_9f3a", Last4: "4242",
	ExpiryMonth: 12, ExpiryYear: 2028,
	CVV: "123", internalRef: "stripe:pm_9f3a",
}
data, _ := json.MarshalIndent(pm, "", "  ")
// CVV, BillingName (vide) et internalRef n'apparaissent pas dans la sortie

Comment gérer time.Time lors de la sérialisation JSON ?

encoding/json sérialise time.Time au format RFC3339Nano par défaut (ex. : "2026-03-10T14:22:00Z"), compatible ISO 8601. Si vous avez besoin d'un format différent — comme des entiers Unix epoch pour une API legacy, ou une chaîne date seule — implémentez MarshalJSON() sur un type wrapper qui embède time.Time et retourne le format dont vous avez besoin.

Go
import (
	"encoding/json"
	"fmt"
	"time"
)

// Comportement par défaut — RFC3339Nano, sans code personnalisé
type AuditEvent struct {
	Action    string    `json:"action"`
	OccurredAt time.Time `json:"occurred_at"`
}

e := AuditEvent{
	Action:    "facture.payée",
	OccurredAt: time.Date(2026, 3, 10, 14, 22, 0, 0, time.UTC),
}
data, _ := json.MarshalIndent(e, "", "  ")
// {
//   "action": "facture.payée",
//   "occurred_at": "2026-03-10T14:22:00Z"
// }

// Personnalisé : timestamp Unix comme entier
type UnixTime struct{ time.Time }

func (u UnixTime) MarshalJSON() ([]byte, error) {
	return []byte(fmt.Sprintf("%d", u.Unix())), nil
}

Outils associés

Aussi disponible en :PythonJavaScriptBash
JO
James OkaforSystems Engineer

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.

TM
Tobias MüllerRéviseur technique

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.