JSON Formatter Go β€” MarshalIndent() Handleiding

Β·Systems EngineerΒ·Beoordeeld doorTobias MΓΌllerΒ·Gepubliceerd

Gebruik de gratis JSON Formatter & Beautifier direct in je browser β€” geen installatie nodig.

JSON Formatter & Beautifier online uitproberen β†’

Wanneer ik aan een Go-microservice werk en een API-respons of configuratiebestand wil inspecteren, is gecomprimeerde JSON het eerste obstakel β€” een enkele regel met honderden geneste velden vertelt me op het eerste gezicht bijna niets. Om JSON te formatteren in Go geeft de standaardbibliotheek je alles wat je nodig hebt: json.MarshalIndent zit ingebakken in encoding/json, wordt meegeleverd met elke Go-installatie en vereist nul externe afhankelijkheden. Deze gids behandelt het volledige plaatje: struct tags, aangepaste MarshalJSON()-implementaties, json.Indent voor het herformatteren van ruwe bytes, streamen van grote bestanden met json.Decoder, wanneer je go-json inzet voor hoge-doorvoerpaden, plus CLI-oneliners voor snel formatteren in de terminal. Alle voorbeelden gebruiken Go 1.21+.

  • βœ“json.MarshalIndent(v, "", "\t") is standaardbibliotheek β€” nul afhankelijkheden, meegeleverd met elke Go-installatie.
  • βœ“Struct tags json:"field_name,omitempty" bepalen serialisatiesleutels en laten zero-value-velden weg uit de uitvoer.
  • βœ“Implementeer MarshalJSON() op elk type om de JSON-representatie volledig te beheersen.
  • βœ“json.Indent() herformatteert al gemarshalede []byte zonder de struct opnieuw te parsen β€” sneller voor ruwe bytes.
  • βœ“Voor grote bestanden (>100 MB): gebruik json.Decoder met Token() om te streamen zonder alles in geheugen te laden.
  • βœ“go-json is een drop-in vervanging 3–5Γ— sneller dan encoding/json voor hoge-doorvoer-API's.

Wat is JSON-formattering?

JSON-formattering β€” ook wel pretty-printing genoemd β€” transformeert een compacte, geminimaliseerde JSON-string naar een voor mensen leesbare indeling met consistente inspringing en regelafbrekingen. De onderliggende data is identiek; alleen de witruimte verandert. Compacte JSON is optimaal voor netwerkoverdracht waarbij elke byte telt; geformatteerde JSON is optimaal voor debuggen, code review, loginspectie en het schrijven van configuratiebestanden. Het encoding/json-pakket van Go verwerkt beide modi met één functieaanroep β€” schakel tussen compact en ingesprongen uitvoer door te kiezen tussen json.Marshal en json.MarshalIndent.

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

json.MarshalIndent() β€” De standaardbibliotheek-aanpak

json.MarshalIndent bevindt zich in het encoding/json-pakket, dat deel uitmaakt van de Go-standaardbibliotheek β€” geen go get nodig. De signatuur is MarshalIndent(v any, prefix, indent string) ([]byte, error): de prefix-string wordt voor elke uitvoeregel geplaatst (bijna altijd leeg gelaten) en indent wordt eenmaal per nestingniveau herhaald. Geef "\t" voor tabs of " " voor twee spaties.

Go β€” minimaal werkend voorbeeld
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"
// 	]
// }

De keuze tussen tabs en spaties is vooral een teamconventie. Veel Go-projecten geven de voorkeur aan tabs omdat gofmt (dat Go-broncode opmaakt) ook tabs gebruikt. Twee of vier spaties zijn gebruikelijk wanneer de JSON bestemd is voor een JavaScript- of Python-consumer. Hier is dezelfde struct met beide inspring-stijlen naast elkaar:

Go β€” tabs vs. spaties
// Tabinspringing β€” voorkeur in Go-native tooling
data, _ := json.MarshalIndent(cfg, "", "	")
// {
// 	"host": "payments-api.internal",
// 	"port": 8443
// }

// Twee spaties β€” gangbaar voor API's met JS/Python-consumers
data, _ = json.MarshalIndent(cfg, "", "  ")
// {
//   "host": "payments-api.internal",
//   "port": 8443
// }

// Vier spaties
data, _ = json.MarshalIndent(cfg, "", "    ")
// {
//     "host": "payments-api.internal",
//     "port": 8443
// }
Opmerking:Gebruik json.Marshal(v) wanneer je compacte uitvoer nodig hebt β€” netwerklading, cachewaarden, of elk pad waarbij de binaire grootte telt. Het accepteert hetzelfde waardeargument en heeft dezelfde foutsemantieken, maar produceert JSON op één regel zonder witruimte.

Struct Tags β€” Veldnamen en Omitempty beheren

Go struct tags zijn stringliteralen die achter velddeclaraties worden geplaatst enencoding/jsonvertellen hoe elk veld geserialiseerd moet worden. Er zijn drie sleutelrichtlijnen: json:"name" hernoemt het veld in de uitvoer, omitempty laat het veld weg wanneer het de nulwaarde voor zijn type heeft, en json:"-" sluit het veld volledig uit β€” handig voor wachtwoorden, interne identificatoren, of velden die de servicegrens nooit mogen oversteken.

Go β€” struct tags voor een API-respons
type UserProfile struct {
	ID          string  `json:"id"`
	Email       string  `json:"email"`
	DisplayName string  `json:"display_name,omitempty"`  // weglaten indien lege string
	AvatarURL   *string `json:"avatar_url,omitempty"`    // weglaten indien nil-pointer
	IsAdmin     bool    `json:"is_admin,omitempty"`      // weglaten indien false
	passwordHash string                                   // unexported β€” automatisch uitgesloten
}

// Gebruiker met alle optionele velden ingevuld
full := UserProfile{
	ID: "usr_7b3c", Email: "ops@example.com",
	DisplayName: "Jan de Vries", IsAdmin: true,
}
// {
//   "id": "usr_7b3c",
//   "email": "ops@example.com",
//   "display_name": "Jan de Vries",
//   "is_admin": true
// }

// Gebruiker zonder optionele velden β€” volledig weggelaten
minimal := UserProfile{ID: "usr_2a91", Email: "dev@example.com"}
// {
//   "id": "usr_2a91",
//   "email": "dev@example.com"
// }

De json:"-" tag is de juiste keuze voor velden die onvoorwaardelijk uitgesloten moeten worden, ongeacht hun waarde β€” doorgaans geheimen, interne volgvelden, of data die in geheugen correct is maar nooit naar een extern systeem geserialiseerd mag worden.

Go β€” gevoelige velden uitsluiten
type AuthToken struct {
	TokenID      string `json:"token_id"`
	Subject      string `json:"sub"`
	IssuedAt     int64  `json:"iat"`
	ExpiresAt    int64  `json:"exp"`
	SigningKey    []byte `json:"-"`   // nooit geserialiseerd
	RefreshToken string `json:"-"`   // nooit geserialiseerd
}

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 en RefreshToken verschijnen nooit
Opmerking:Unexported (kleine letter) struct-velden worden altijd uitgesloten door encoding/json, ongeacht eventuele tags. Je hoeft json:"-" niet aan unexported velden toe te voegen β€” de uitsluiting is automatisch en kan niet worden overschreven.

Aangepaste MarshalJSON() β€” Niet-standaard typen verwerken

Elk Go-type kan de json.Marshaler-interface implementeren door een MarshalJSON() ([]byte, error)-methode te definiΓ«ren. Wanneer encoding/json zo'n type tegenkomt, roept het de methode aan in plaats van zijn standaard reflectie-gebaseerde marshaling. Dit is het canonieke Go-patroon voor domeintypen die een specifieke wire-representatie nodig hebben β€” monetaire waarden, status-enums, aangepaste tijdformaten, of elk type dat data anders opslaat dan hoe het geserialiseerd moet worden.

Aangepast type β€” Money met centen-naar-decimalen-omrekening

Go β€” aangepaste MarshalJSON voor Money
package main

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

type Money struct {
	Amount   int64  // opgeslagen in centen om drijvende-komma-drift te vermijden
	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" }
// }

Status-enum β€” stringrepresentatie

Go β€” aangepaste MarshalJSON voor status-enum
type OrderStatus int

const (
	StatusPending OrderStatus = iota
	StatusPaid
	StatusShipped
	StatusCancelled
)

var orderStatusNames = map[OrderStatus]string{
	StatusPending:   "pending",
	StatusPaid:      "paid",
	StatusShipped:   "shipped",
	StatusCancelled: "cancelled",
}

func (s OrderStatus) MarshalJSON() ([]byte, error) {
	name, ok := orderStatusNames[s]
	if !ok {
		return nil, fmt.Errorf("unknown order status: %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": "shipped"
// }
Opmerking:Implementeer altijd zowel MarshalJSON() als UnmarshalJSON() samen. Als je alleen marshaling implementeert, zal het rondsturen van het type via JSON (serialiseren β†’ opslaan β†’ deserialiseren) stilletjes structuur verliezen of het verkeerde type teruggeven. Het paar vormt een contract dat het type een JSON-rondreis kan overleven.

UUID β€” serialiseren als string

Go's standaardbibliotheek heeft geen UUID-type. De meest gebruikte keuze is github.com/google/uuid, dat al MarshalJSON() implementeert en serialiseert als een geciteerde RFC 4122-string. Als je een ruwe [16]byte of een aangepast ID-type gebruikt, implementeer dan zelf de interface om base64-gecodeerde binaire blobs in je JSON-uitvoer te vermijden.

Go 1.21+ β€” UUID-serialisatie
import (
    "encoding/json"
    "fmt"

    "github.com/google/uuid"
)

type AuditEvent struct {
    EventID   uuid.UUID `json:"event_id"`   // serialiseert als "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:    "user.password_changed",
    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": "user.password_changed",
//   "actor_id": "usr_7f3a91bc",
//   "occurred_at": "2026-03-10T14:22:00Z"
// }
Opmerking:Als je UUID's opslaat als [16]byte zonder aangepast type, codeert encoding/json ze als base64-string β€” bijv. "VQ6EAOKbQdSnFkRmVUQAAA==". Gebruik altijd een correct UUID-type of implementeer MarshalJSON() om het canonieke koppelteken-formaat te produceren.

json.MarshalIndent() Parameters referentie

De functiesignatuur is func MarshalIndent(v any, prefix, indent string) ([]byte, error). Zowel prefix als indent zijn stringliteralen β€” er is geen numerieke afkorting zoals Python's indent=4.

Parameter
Type
Beschrijving
v
any
De te marshalen waarde β€” struct, map, slice of primitief type
prefix
string
String die vΓ³Γ³r elke uitvoeregel wordt geplaatst (meestal "")
indent
string
String die per inspringstap wordt herhaald ("\t" of " ")

Veelgebruikte struct tag-opties:

Tag
Effect
json:"name"
Rename field to name in JSON output
json:"name,omitempty"
Rename + omit if zero value (nil, "", 0, false)
json:"-"
Always exclude this field from JSON output
json:",string"
Encode number or bool as a JSON string value

json.Indent() β€” Bestaande JSON-bytes herformatteren

Wanneer je al een []byte van JSON hebt β€” zeg van een HTTP-responsebody, een Postgres jsonb-kolom, of een bestand ingelezen met os.ReadFile β€” hoef je geen struct te definiΓ«ren en te unmarshalen voordat je mooi kunt afdrukken. json.Indent herformatteert de ruwe bytes direct door ingesprongen uitvoer te schrijven naar een bytes.Buffer.

Go β€” json.Indent op ruwe bytes
package main

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

func main() {
	// Een ruwe JSON-payload van een upstream-dienst simuleren
	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
// }

Een patroon dat ik vaak gebruik in microservices is json.Indent aanroepen voordat ik naar gestructureerde logs schrijf β€” het voegt verwaarloosbare overhead toe en maakt logvermeldingen veel gemakkelijker te lezen tijdens een incident. De functie is bijzonder nuttig voor het loggen van HTTP-responsen, het mooi afdrukken van opgeslagen JSON-strings, en format-on-read-pijplijnen waarbij de structdefinitie niet beschikbaar is.

Go β€” json.Indent voor debuglogboekregistratie
func logResponse(logger *slog.Logger, statusCode int, body []byte) {
	var pretty bytes.Buffer
	if err := json.Indent(&pretty, body, "", "  "); err != nil {
		// Body is geen geldige JSON β€” log ruw
		logger.Debug("upstream response", "status", statusCode, "body", string(body))
		return
	}
	logger.Debug("upstream response", "status", statusCode, "body", pretty.String())
}
Waarschuwing:json.Indent valideert de JSON NIET volledig voorbij wat structureel nodig is om witruimte in te voegen. Voor volledige syntaxvalidatie, roep eerst json.Valid(data) aan en verwerk het false-geval voordat je probeert te indenteren.

JSON formatteren vanuit een bestand en HTTP-respons

Twee van de meest voorkomende realistische scenario's in Go-services zijn het formatteren van JSON gelezen uit een bestand op schijf (configuratiebestanden, fixture-data, migratiezaad) en het mooi afdrukken van HTTP-responsbodies voor debuglogboekregistratie of testbeweringen. Beide volgen hetzelfde patroon: bytes lezen, aanroepen json.Indent of unmarshalen dan json.MarshalIndent, terugschrijven of loggen.

Bestand lezen β†’ Formatteren β†’ Terugschrijven

Go β€” JSON-bestand in-place formatteren
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("read %s: %w", path, err)
	}

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

func main() {
	if err := formatJSONFile("config/database.json"); err != nil {
		log.Fatalf("format config: %v", err)
	}
	fmt.Println("config/database.json succesvol opgemaakt")
}

HTTP-respons β†’ Decoderen β†’ Pretty-print voor debuglog

Go β€” HTTP-respons formatteren voor debuglog
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("decode health response: %v", err)
	}

	pretty, err := json.MarshalIndent(result, "", "  ")
	if err != nil {
		log.Fatalf("marshal health response: %v", err)
	}
	fmt.Printf("Gezondheidscontrole (%d):
%s
", resp.StatusCode, string(pretty))
}
// Gezondheidscontrole (200):
// {
//   "status": "ok",
//   "version": "1.4.2",
//   "checks": {
//     "database": "ok",
//     "cache": "ok",
//     "queue": "degraded"
//   },
//   "uptime_seconds": 172800
// }

Ruwe responsebody β†’ json.Indent (geen Struct vereist)

Go β€” ruwe HTTP-body formatteren met 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("read body: %v", err)
	}

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

JSON mooi afdrukken vanuit een HTTP-respons in Go

De twee bovenstaande benaderingen dekken de meest voorkomende gevallen: decoderen naar een getypeerde struct en dan json.MarshalIndent aanroepen (het beste wanneer je specifieke velden moet valideren of inspecteren), of de ruwe body-bytes lezen met io.ReadAll en json.Indent direct aanroepen (het beste voor snel debugloggen wanneer je geen structdefinitie bij de hand hebt). De ruwe-bytes-benadering is eenvoudiger maar geeft je geen Go-typeveiligheid of veldtoegang β€” het is puur een weergavetransformatie. Beide benaderingen verwerken grote responsbodies correct zolang de volledige body in het geheugen past.

Go β€” twee patronen naast elkaar
// Patroon A: getypeerde decodering β†’ MarshalIndent
// Gebruik dit wanneer je specifieke velden moet inspecteren of valideren
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
pretty, _ := json.MarshalIndent(result, "", "  ")
fmt.Println(string(pretty))

// Patroon B: ruwe bytes β†’ json.Indent
// Gebruik voor snel debugloggen β€” geen structdefinitie vereist
body, _ := io.ReadAll(resp.Body)
var buf bytes.Buffer
json.Indent(&buf, body, "", "  ")
fmt.Println(buf.String())

Opdrachtregelformattering van JSON in Go-projecten

Soms moet je een JSON-payload direct in de terminal formatteren zonder een Go-programma te schrijven. Dit zijn de oneliners die ik uit mijn hoofd ken tijdens ontwikkeling en incidentrespons.

bash β€” JSON naar Python's ingebouwde formatter pipen
echo '{"service":"payments","port":8443,"workers":4}' | python3 -m json.tool
# {
#     "service": "payments",
#     "port": 8443,
#     "workers": 4
# }
bash β€” naar jq pipen voor volledig uitgerust formatteren en filteren
# Alleen formatteren
cat api-response.json | jq .

# Een genest veld extraheren
cat api-response.json | jq '.checks.database'

# Een array filteren
cat audit-log.json | jq '.[] | select(.severity == "error")'
bash β€” naar een minimale Go main.go via stdin pipen
# main.go: leest stdin, formatteert, schrijft 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
Opmerking:gofmt formatteert Go-broncode, niet JSON. Pipe JSON-bestanden niet door gofmt β€” het zal ofwel een fout geven of onherkenbare uitvoer produceren. Gebruik jq . of python3 -m json.tool voor JSON-bestanden.

Hoogperformant alternatief β€” go-json

Voor de overgrote meerderheid van Go-services is encoding/json snel genoeg. Maar als JSON-marshaling opduikt in je profiler β€” veelvoorkomend in hoge-doorvoer REST API's of services die per verzoek grote gestructureerde logregels uitstoten β€” is de go-json-bibliotheek een drop-in vervanging die 3–5Γ— sneller is met een identiek API-oppervlak.

bash β€” go-json installeren
go get github.com/goccy/go-json
Go β€” encoding/json vervangen door go-json met één import-wijziging
package main

import (
	// Vervang dit:
	// "encoding/json"

	// Door dit β€” identieke API, geen andere codewijzigingen:
	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: "invoice.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))
	// Uitvoer is identiek aan encoding/json β€” alleen de snelheid verschilt
}

github.com/bytedance/sonic is de snelste Go JSON-bibliotheek beschikbaar, maar draait alleen op amd64 en arm64 (het gebruikt JIT-compilatie). Gebruik go-json wanneer je een draagbare drop-in nodig hebt; grijp naar sonic wanneer je op een bekende architectuur draait en elke microseconde in een hot path nodig hebt.

Werken met grote JSON-bestanden

Zowel json.MarshalIndent als json.Indent vereisen dat de volledige payload in het geheugen zit. Voor bestanden boven 100 MB β€” data-exports, auditlogs, Kafka-consumerpayloads β€” gebruik json.Decoder om de invoer te streamen en records één voor één te verwerken.

Een grote JSON-array streamen met json.Decoder

Go β€” grote JSON-array streamen zonder in geheugen te laden
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("open %s: %w", path, err)
	}
	defer file.Close()

	dec := json.NewDecoder(file)

	// Lees de openende '['
	if _, err := dec.Token(); err != nil {
		return fmt.Errorf("read opening token: %w", err)
	}

	var processed int
	for dec.More() {
		var event AuditEvent
		if err := dec.Decode(&event); err != nil {
			return fmt.Errorf("decode event %d: %w", processed, err)
		}
		// Verwerk één event tegelijk β€” constant geheugengebruik
		if event.Severity == "error" {
			fmt.Printf("[ERROR] %s: %s (%dms)
", event.UserID, event.Action, event.DurationMs)
		}
		processed++
	}
	fmt.Printf("%d auditgebeurtenissen verwerkt
", processed)
	return nil
}

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

NDJSON β€” één JSON-object per regel

Go β€” NDJSON-logstroom regel voor regel verwerken
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("open log: %v", err)
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	scanner.Buffer(make([]byte, 1024*1024), 1024*1024) // 1 MB per regel

	for scanner.Scan() {
		var line LogLine
		if err := json.Unmarshal(scanner.Bytes(), &line); err != nil {
			continue // sla misvormde regels over
		}
		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)
	}
}
Opmerking:Schakel over naar streaming wanneer het bestand groter is dan 100 MB of wanneer je onbegrensde streams verwerkt (Kafka-consumers, logpijplijnen, S3-objectlezingen). Een 500 MB JSON-bestand laden met os.ReadFile wijst die volledige buffer toe op de heap, veroorzaakt GC-druk en kan OOM veroorzaken op geheugenbeperkte containers.

Veelgemaakte fouten

❌ De foutretournering van MarshalIndent negeren

Probleem: De fout verwijderen met _ betekent dat een niet-serialiseerbare waarde (een kanaal, functie, complex getal) stilletjes nil-uitvoer produceert of downstream een panic veroorzaakt wanneer je string(nil) aanroept.

Oplossing: Controleer altijd de fout. Als je een type marshaalt dat altijd moet slagen, is een panikerend log.Fatalf beter dan stil gegevensverlies.

Before Β· Go
After Β· Go
data, _ := json.MarshalIndent(payload, "", "  ")
fmt.Println(string(data)) // lege string als marshal mislukte
data, err := json.MarshalIndent(payload, "", "  ")
if err != nil {
    log.Fatalf("marshal payload: %v", err)
}
fmt.Println(string(data))
❌ fmt.Println gebruiken in plaats van os.Stdout.Write voor binaire uitvoer

Probleem: fmt.Println(string(data)) voegt een newline-teken toe na de JSON, wat pijplijnen corrumpeert die de uitvoer als ruwe bytes behandelen β€” bijvoorbeeld bij pipen naar jq of schrijven naar een binair protocol.

Oplossing: Gebruik os.Stdout.Write(data) voor binair-schone uitvoer. Als je een afsluitende newline nodig hebt voor menselijke weergave, voeg die dan expliciet toe.

Before Β· Go
After Β· Go
data, _ := json.MarshalIndent(cfg, "", "  ")
fmt.Println(string(data)) // voegt een extra newline aan het einde toe
data, _ := json.MarshalIndent(cfg, "", "  ")
os.Stdout.Write(data)
os.Stdout.Write([]byte("
")) // expliciete newline alleen wanneer nodig
❌ omitempty vergeten op pointervelden

Probleem: Zonder omitempty serialiseert een nil *string of *int-pointer als "field": null. Dit legt interne veldnamen bloot en kan strenge JSON-schemavalidatoren aan de consumentenzijde breken.

Oplossing: Voeg omitempty toe aan pointervelden die je wilt weglaten (niet null) in de uitvoer. Een nil *T met omitempty produceert helemaal geen sleutel in de JSON.

Before Β· Go
After Β· Go
type WebhookPayload struct {
    EventID   string  `json:"event_id"`
    ErrorMsg  *string `json:"error_msg"`  // verschijnt als null wanneer nil
}
// {"event_id":"evt_3c7f","error_msg":null}
type WebhookPayload struct {
    EventID   string  `json:"event_id"`
    ErrorMsg  *string `json:"error_msg,omitempty"`  // weggelaten wanneer nil
}
// {"event_id":"evt_3c7f"}
❌ map[string]interface{} gebruiken in plaats van structs

Probleem: Unmarshalen naar map[string]any verliest type-informatie, vereist handmatige type-assertions en produceert niet-deterministische sleutelvolgorde β€” waardoor JSON-diffs en logvergelijkingen moeilijker worden.

Oplossing: Definieer een struct met de juiste json-tags. Structs zijn typeveilig, marshalen sneller, produceren deterministische veldvolgorde die overeenkomt met de structdefinitie en maken de code zelfdocumenterend.

Before Β· Go
After Β· Go
var result map[string]any
json.Unmarshal(body, &result)
port := result["port"].(float64) // type-assertion vereist, paniceert bij verkeerd 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 // getypeerd, veilig, snel

encoding/json vs. alternatieven β€” Snelle vergelijking

Method
Opgemaakte uitvoer
Valid JSON
Aangepaste typen
Streaming
Installatie
json.MarshalIndent
βœ“
βœ“
βœ“ via MarshalJSON
βœ—
Nee (stdlib)
json.Indent
βœ“
βœ“
N/A (alleen bytes)
βœ—
Nee (stdlib)
json.Encoder
βœ— (compact)
βœ“
βœ“ via MarshalJSON
βœ“
Nee (stdlib)
go-json
βœ“
βœ“
βœ“
βœ“
go get
sonic
βœ“
βœ“
βœ“
βœ“
go get (amd64/arm64)
jq (CLI)
βœ“
βœ“
N/A
βœ“
Systeeminstallatie

Gebruik json.MarshalIndent voor elk geval waarbij je de structdefinitie beheert en geformatteerde uitvoer nodig hebt β€” configuratiebestanden, debuglogboekregistratie, testfixtures en API-responslogboekregistratie. Gebruik json.Indent wanneer je al ruwe bytes hebt en alleen witruimte wilt toevoegen zonder een rondreis via Go-typen. Schakel pas over naar go-json of sonic nadat profilering bevestigt dat JSON-marshaling een meetbare knelpunt is β€” voor de meeste services is de standaardbibliotheek meer dan voldoende.

Veelgestelde vragen

Hoe print ik JSON mooi af in Go?

Roep json.MarshalIndent(v, "", "\t") aan uit het encoding/json-pakket β€” het tweede argument is een regelprefix (meestal leeg) en het derde is de inspringing per niveau. Geef "\t" voor tabs of " " voor twee spaties. Er is geen externe bibliotheek nodig; encoding/json zit in de standaardbibliotheek van Go.

Go
package main

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

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

Wat is het verschil tussen json.Marshal en json.MarshalIndent?

json.Marshal produceert compacte JSON op één regel zonder witruimte β€” ideaal voor netwerkoverdracht waarbij elke byte telt. json.MarshalIndent neemt twee extra stringparameters (prefix en indent) en produceert ingesprongen, voor mensen leesbare uitvoer. Beide functies accepteren dezelfde waardetypen en retourneren ([]byte, error). De enige extra kosten van MarshalIndent zijn iets meer uitvoerbytes en een verwaarloosbare hoeveelheid extra CPU voor het invoegen van witruimte.

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

Hoe formatteer ik een JSON []byte zonder de struct te unmarshalen?

Gebruik json.Indent(&buf, src, "", "\t"). Deze functie neemt een bestaande []byte van JSON en schrijft de ingesprongen versie naar een bytes.Buffer β€” geen structdefinitie nodig, geen type-assertions, geen heen-en-weer via Go-typen. Dit is de snelste optie als je al ruwe JSON-bytes hebt, zoals van een HTTP-responsebody of een databasekolom.

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

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

Waarom retourneert json.MarshalIndent een fout?

encoding/json retourneert een fout wanneer de waarde niet als JSON kan worden weergegeven. De meest voorkomende oorzaken zijn: het marshalen van een kanaal, functie of complex getal (deze hebben geen JSON-equivalent); een struct die MarshalJSON() implementeert en een fout retourneert; of een map met niet-stringsleutels. Belangrijk: het marshalen van een struct met een unexported of nil-pointerveld veroorzaakt GEEN fout β€” die worden simpelweg weggelaten.

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

// Dit geeft een fout β€” kanalen zijn niet JSON-serialiseerbaar
ch := make(chan int)
_, err := json.MarshalIndent(ch, "", "	")
fmt.Println(err)
// json: unsupported type: chan int

// Dit is prima β€” nil-pointervelden met omitempty worden stil weggelaten
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 weggelaten, geen fout

Hoe sluit ik een veld uit van JSON-uitvoer in Go?

Er zijn drie manieren. Ten eerste, gebruik de json:"-" struct tag β€” het veld wordt altijd uitgesloten, ongeacht de waarde. Ten tweede, gebruik omitempty β€” het veld wordt alleen uitgesloten wanneer het de nulwaarde voor zijn type heeft (nil-pointer, lege string, 0, false). Ten derde worden unexported (kleine letter) velden automatisch uitgesloten door encoding/json, zonder dat er een tag nodig is.

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:"-"`                    // altijd uitgesloten
	BillingName  string  `json:"billing_name,omitempty"` // uitgesloten indien leeg
	internalRef  string                                 // unexported β€” automatisch uitgesloten
}

pm := PaymentMethod{
	ID: "pm_9f3a", Last4: "4242",
	ExpiryMonth: 12, ExpiryYear: 2028,
	CVV: "123", internalRef: "stripe:pm_9f3a",
}
data, _ := json.MarshalIndent(pm, "", "  ")
// CVV, BillingName (leeg) en internalRef verschijnen niet in de uitvoer

Hoe ga ik om met time.Time bij JSON-marshaling?

encoding/json marshaalt time.Time standaard naar RFC3339Nano-formaat (bijv. "2026-03-10T14:22:00Z"), wat ISO 8601-compatibel is. Als je een ander formaat nodig hebt β€” zoals Unix-epoch-integers voor een legacy-API, of een aangepaste datum-string β€” implementeer dan MarshalJSON() op een wrapper-type dat time.Time insluit en het gewenste formaat retourneert.

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

// Standaardgedrag β€” RFC3339Nano, geen aangepaste code nodig
type AuditEvent struct {
	Action    string    `json:"action"`
	OccurredAt time.Time `json:"occurred_at"`
}

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

// Aangepast: Unix-tijdstempel als integer
type UnixTime struct{ time.Time }

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

Gerelateerde tools

Ook beschikbaar in: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ΓΌllerTechnisch beoordelaar

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.