JSON Formatter Go — MarshalIndent() Guide

·Systems Engineer·Geprüft vonTobias Müller·Veröffentlicht

Nutze das kostenlose JSON Formatter & Beautifier direkt im Browser – keine Installation erforderlich.

JSON Formatter & Beautifier online testen →

Wenn ich an einem Go-Microservice arbeite und eine API-Antwort oder eine Konfigurationsdatei inspizieren muss, ist kompaktes JSON das erste Hindernis — eine einzige Zeile mit Hunderten verschachtelter Felder sagt mir auf den ersten Blick fast nichts. Um JSON in Go zu formatieren, bietet die Standardbibliothek alles Notwendige: json.MarshalIndent ist Teil von encoding/json, ist in jeder Go-Installation enthalten und benötigt keinerlei Drittanbieter-Abhängigkeiten. Dieser Leitfaden deckt das gesamte Bild ab: Struct Tags, benutzerdefinierte MarshalJSON()-Implementierungen, json.Indent zum Neuformatieren roher Bytes, das Streaming großer Dateien mit json.Decoder, und wann man für hochdurchsatzkritische Pfade auf go-json zurückgreifen sollte — plus CLI-Einzeiler für schnelle Formatierung im Terminal. Alle Beispiele verwenden Go 1.21+.

  • json.MarshalIndent(v, "", "\t") ist Standardbibliothek — null Abhängigkeiten, in jeder Go-Installation enthalten.
  • Struct Tags json:"field_name,omitempty" steuern Serialisierungsschlüssel und lassen Nullwert-Felder weg.
  • MarshalJSON() auf einem beliebigen Typ implementieren, um die JSON-Darstellung vollständig zu kontrollieren.
  • json.Indent() formatiert bereits marshallierte []byte neu, ohne das Struct erneut zu parsen — schneller für rohe Bytes.
  • Für große Dateien (>100 MB): json.Decoder mit Token() verwenden, um ohne vollständiges In-Memory-Laden zu streamen.
  • go-json ist ein Drop-in-Ersatz, der 3–5× schneller als encoding/json ist — geeignet für High-Throughput-APIs.

Was ist JSON-Formatierung?

JSON-Formatierung — auch Pretty-Printing genannt — wandelt einen kompakten, minimierten JSON-String in ein lesbar strukturiertes Layout mit konsistenter Einrückung und Zeilenumbrüchen um. Die zugrunde liegenden Daten sind identisch; nur der Leerraum ändert sich. Kompaktes JSON ist optimal für die Netzwerkübertragung, wo jedes Byte zählt; formatiertes JSON ist optimal für Debugging, Code-Reviews, Log-Inspektion und das Verfassen von Konfigurationsdateien. Das encoding/json-Paket in Go unterstützt beide Modi mit einem einzigen Funktionsaufruf — zwischen kompakter und eingerückter Ausgabe wechseln durch die Wahl zwischen json.Marshal und json.MarshalIndent.

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

json.MarshalIndent() — Der Standardbibliothek-Ansatz

json.MarshalIndent lebt im Paket encoding/json, das zur Go-Standardbibliothek gehört — kein go get erforderlich. Die Signatur lautet MarshalIndent(v any, prefix, indent string) ([]byte, error): der prefix-String wird jeder Ausgabezeile vorangestellt (fast immer leer gelassen), und indent wird einmal pro Verschachtelungsebene wiederholt. Man übergibt "\t" für Tabs oder " " für zwei Leerzeichen.

Go — minimales funktionsfähiges Beispiel
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"
// 	]
// }

Die Wahl zwischen Tabs und Leerzeichen ist meist eine Teamkonvention. Viele Go-Projekte bevorzugen Tabs, weil gofmt (das Go-Quellcode formatiert) Tabs verwendet. Zwei- oder Vier-Leerzeichen-Einrückung ist üblich, wenn das JSON für einen JavaScript- oder Python-Consumer bestimmt ist. Hier ist dasselbe Struct mit beiden Einrückungsstilen nebeneinander:

Go — Tabs vs. Leerzeichen
// Tab-Einrückung — bevorzugt in Go-nativen Tools
data, _ := json.MarshalIndent(cfg, "", "	")
// {
// 	"host": "payments-api.internal",
// 	"port": 8443
// }

// Zwei-Leerzeichen-Einrückung — gängig für APIs mit JS/Python-Consumern
data, _ = json.MarshalIndent(cfg, "", "  ")
// {
//   "host": "payments-api.internal",
//   "port": 8443
// }

// Vier-Leerzeichen-Einrückung
data, _ = json.MarshalIndent(cfg, "", "    ")
// {
//     "host": "payments-api.internal",
//     "port": 8443
// }
Hinweis:json.Marshal(v) verwenden, wenn kompakte Ausgabe benötigt wird — Netzwerk-Payloads, Cache-Werte oder jeden Pfad, bei dem die Binärgröße eine Rolle spielt. Es akzeptiert dasselbe Wertargument und hat dieselbe Fehler-Semantik, erzeugt jedoch einzeiliges JSON ohne Leerraum.

Struct Tags — Feldnamen und Omitempty steuern

Go-Struct-Tags sind Zeichenkettenliterale, die nach Felddeklarationen stehen undencoding/jsonmitteilen, wie jedes Feld serialisiert werden soll. Es gibt drei Schlüsseldirektiven: json:"name" benennt das Feld in der Ausgabe um, omitempty lässt das Feld weg, wenn es den Nullwert seines Typs enthält, und json:"-" schließt das Feld vollständig aus — nützlich für Passwörter, interne Bezeichner oder Felder, die die Service-Grenze niemals verlassen dürfen.

Go — struct tags für eine API-Antwort
type UserProfile struct {
	ID          string  `json:"id"`
	Email       string  `json:"email"`
	DisplayName string  `json:"display_name,omitempty"`  // weglassen wenn leerer String
	AvatarURL   *string `json:"avatar_url,omitempty"`    // weglassen wenn nil-Pointer
	IsAdmin     bool    `json:"is_admin,omitempty"`      // weglassen wenn false
	passwordHash string                                   // nicht exportiert — auto ausgeschlossen
}

// Benutzer mit allen optionalen Feldern befüllt
full := UserProfile{
	ID: "usr_7b3c", Email: "l.bauer@beispiel.de",
	DisplayName: "Lukas Bauer", IsAdmin: true,
}
// {
//   "id": "usr_7b3c",
//   "email": "l.bauer@beispiel.de",
//   "display_name": "Lukas Bauer",
//   "is_admin": true
// }

// Benutzer ohne optionale Felder — sie werden vollständig weggelassen
minimal := UserProfile{ID: "usr_2a91", Email: "s.mueller@beispiel.de"}
// {
//   "id": "usr_2a91",
//   "email": "s.mueller@beispiel.de"
// }

Der Tag json:"-" ist die richtige Wahl für Felder, die unbedingt ausgeschlossen werden müssen — unabhängig von ihrem Wert — typischerweise Geheimnisse, interne Tracking-Felder oder Daten, die im Speicher korrekt sind, aber niemals an ein externes System serialisiert werden dürfen.

Go — sensible Felder ausschließen
type AuthToken struct {
	TokenID      string `json:"token_id"`
	Subject      string `json:"sub"`
	IssuedAt     int64  `json:"iat"`
	ExpiresAt    int64  `json:"exp"`
	SigningKey    []byte `json:"-"`   // niemals serialisiert
	RefreshToken string `json:"-"`   // niemals serialisiert
}

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 und RefreshToken erscheinen nie
Hinweis:Nicht exportierte (kleingeschriebene) Struct-Felder werden von encoding/json immer ausgeschlossen, unabhängig von Tags. Es ist nicht nötig, json:"-" zu nicht exportierten Feldern hinzuzufügen — der Ausschluss ist automatisch und kann nicht überschrieben werden.

Benutzerdefiniertes MarshalJSON() — Nicht-Standard-Typen behandeln

Jeder Go-Typ kann das Interface json.Marshaler implementieren, indem er eine Methode MarshalJSON() ([]byte, error) definiert. Wenn encoding/json auf einen solchen Typ trifft, ruft es die Methode auf, anstatt sein standardmäßiges reflexionsbasiertes Marshalling zu verwenden. Dies ist das kanonische Go-Muster für Domain-Typen, die eine spezifische Wire-Darstellung benötigen — Geldbeträge, Status-Enums, benutzerdefinierte Zeitformate oder jeden Typ, der Daten anders speichert, als er serialisiert werden soll.

Eigener Typ — Money mit Cent-zu-Dezimal-Konvertierung

Go — benutzerdefiniertes MarshalJSON für Money
package main

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

type Money struct {
	Amount   int64  // in Cent gespeichert, um Gleitkomma-Drift zu vermeiden
	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 — String-Darstellung

Go — benutzerdefiniertes MarshalJSON für 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"
// }
Hinweis:Immer sowohl MarshalJSON() als auch UnmarshalJSON() gemeinsam implementieren. Wenn nur das Marshalling implementiert wird, geht beim Round-Trip des Typs durch JSON (serialisieren → speichern → deserialisieren) stillschweigend Struktur verloren oder es wird der falsche Typ zurückgegeben. Das Paar bildet einen Vertrag, dass der Typ einen JSON-Round-Trip übersteht.

UUID — Als String serialisieren

Die Go-Standardbibliothek hat keinen UUID-Typ. Die gängigste Wahl ist github.com/google/uuid, das bereits MarshalJSON() implementiert und als quoted RFC-4122-String serialisiert. Bei Verwendung eines rohen [16]byte oder eines benutzerdefinierten ID-Typs empfiehlt sich die eigene Implementierung des Interfaces, um Base64-kodierte Binär-Blobs in der JSON-Ausgabe zu vermeiden.

Go 1.21+ — UUID-Serialisierung
import (
    "encoding/json"
    "fmt"

    "github.com/google/uuid"
)

type AuditEvent struct {
    EventID   uuid.UUID `json:"event_id"`   // serialisiert 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"
// }
Hinweis:Werden UUIDs als [16]byte ohne eigenen Typ gespeichert, kodiert encoding/json sie als Base64-String — z. B. "VQ6EAOKbQdSnFkRmVUQAAA==". Immer einen eigenen UUID-Typ verwenden oder MarshalJSON() implementieren, um das kanonische Bindestrich-String-Format auszugeben.

json.MarshalIndent() — Parameterreferenz

Die Funktionssignatur lautet func MarshalIndent(v any, prefix, indent string) ([]byte, error). Sowohl prefix als auch indent sind Zeichenkettenliterale — es gibt keine numerische Kurzschreibweise wie Pythons indent=4.

Parameter
Type
Beschreibung
v
any
Der zu marshallende Wert — Struct, Map, Slice oder Primitiv
prefix
string
Zeichenkette, die jeder Ausgabezeile vorangestellt wird (meist "")
indent
string
Zeichenkette für jede Einrückungsebene ("\t" oder " ")

Häufige Struct-Tag-Optionen:

Tag
Wirkung
json:"name"
Benennt das Feld in der JSON-Ausgabe um
json:"name,omitempty"
Umbenennen + weglassen, wenn Nullwert (nil, "", 0, false)
json:"-"
Schließt dieses Feld immer aus der JSON-Ausgabe aus
json:",string"
Kodiert Zahl oder Bool als JSON-Zeichenkettenwert

json.Indent() — Bestehende JSON-Bytes neuformatieren

Wenn bereits ein []byte mit JSON vorliegt — etwa aus einem HTTP-Response-Body, einer Postgres- jsonb-Spalte oder einer mit os.ReadFile gelesenen Datei — muss kein Struct definiert und kein Unmarshal durchgeführt werden, bevor formatiert werden kann. json.Indent formatiert die rohen Bytes direkt neu, indem es die eingerückte Ausgabe in einen bytes.Buffer schreibt.

Go — json.Indent auf rohen Bytes
package main

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

func main() {
	// Simulierter roher JSON-Payload von einem vorgelagerten Service
	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
// }

Ein Muster, das ich in Microservices häufig einsetze: vor dem Schreiben in strukturierte Logs json.Indent aufrufen — es fügt vernachlässigbaren Overhead hinzu und macht Log-Einträge bei einem Incident wesentlich leichter lesbar. Die Funktion ist besonders nützlich für das Logging von HTTP-Antworten, das Pretty-Printing gespeicherter JSON-Strings und Format-on-Read-Pipelines, bei denen die Struct-Definition nicht verfügbar ist.

Go — json.Indent für Debug-Logging
func logResponse(logger *slog.Logger, statusCode int, body []byte) {
	var pretty bytes.Buffer
	if err := json.Indent(&pretty, body, "", "  "); err != nil {
		// Body ist kein gültiges JSON — roh loggen
		logger.Debug("upstream response", "status", statusCode, "body", string(body))
		return
	}
	logger.Debug("upstream response", "status", statusCode, "body", pretty.String())
}
Warnung:json.Indent validiert das JSON NICHT vollständig, über das hinaus, was strukturell zum Einfügen von Leerzeichen erforderlich ist. Für eine vollständige Syntaxvalidierung erst json.Valid(data) aufrufen und den false-Fall behandeln, bevor versucht wird einzurücken.

JSON aus Datei und HTTP-Antwort formatieren

Zwei der häufigsten Szenarien in Go-Services sind das Formatieren von JSON aus einer Datei auf der Festplatte (Konfigurationsdateien, Fixture-Daten, Migrations-Seeds) und das Pretty-Printing von HTTP-Response-Bodies für Debug-Logging oder Test-Assertions. Beide folgen demselben Muster: Bytes lesen, json.Indent aufrufen oder unmarshalln und dann json.MarshalIndent verwenden, zurückschreiben oder loggen.

Datei lesen → formatieren → zurückschreiben

Go — JSON-Datei in-place formatieren
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/datenbank.json"); err != nil {
		log.Fatalf("format config: %v", err)
	}
	fmt.Println("config/datenbank.json erfolgreich formatiert")
}

HTTP-Antwort → Decode → Pretty-Print für Debug-Logging

Go — HTTP-Antwort für Debug-Log formatieren
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("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
// }

Roher Response-Body → json.Indent (kein Struct erforderlich)

Go — rohen HTTP-Body mit io.ReadAll formatieren
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 aus einer HTTP-Antwort in Go formatiert ausgeben

Die beiden oben gezeigten Ansätze decken die häufigsten Fälle ab: in ein typisiertes Struct dekodieren und dann json.MarshalIndent aufrufen (am besten, wenn Felder validiert oder inspiziert werden müssen), oder die rohen Body-Bytes mit io.ReadAll lesen und direkt json.Indent aufrufen (am besten für schnelles Debug-Logging, wenn keine Struct-Definition verfügbar ist). Der Raw-Bytes-Ansatz ist einfacher, bietet aber keine Go-Typsicherheit oder Feldzugriff — es ist eine rein visuelle Transformation. Beide Ansätze verarbeiten große Response-Bodies korrekt, solange der gesamte Body in den Speicher passt.

Go — zwei Muster nebeneinander
// Muster A: typisiertes Decode → MarshalIndent
// Verwenden, wenn bestimmte Felder inspiziert oder validiert werden müssen
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
pretty, _ := json.MarshalIndent(result, "", "  ")
fmt.Println(string(pretty))

// Muster B: rohe Bytes → json.Indent
// Für schnelles Debug-Logging — keine Struct-Definition erforderlich
body, _ := io.ReadAll(resp.Body)
var buf bytes.Buffer
json.Indent(&buf, body, "", "  ")
fmt.Println(buf.String())

JSON-Formatierung über die Kommandozeile in Go-Projekten

Manchmal muss ein JSON-Payload direkt im Terminal formatiert werden, ohne ein Go-Programm zu schreiben. Diese Einzeiler haben sich bei mir in der Entwicklung und bei der Incident-Response als unentbehrlich erwiesen.

bash — JSON an Pythons integrierten Formatter pipen
echo '{"service":"payments","port":8443,"workers":4}' | python3 -m json.tool
# {
#     "service": "payments",
#     "port": 8443,
#     "workers": 4
# }
bash — an jq pipen für vollständige Formatierung und Filterung
# Nur formatieren
cat api-response.json | jq .

# Verschachteltes Feld extrahieren
cat api-response.json | jq '.checks.database'

# Array filtern
cat audit-log.json | jq '.[] | select(.severity == "error")'
bash — an minimales Go main.go via stdin pipen
# main.go: liest stdin, formatiert, schreibt 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
Hinweis:gofmt formatiert Go-Quellcode, nicht JSON. JSON-Dateien nicht durch gofmt pipen — es wird entweder einen Fehler erzeugen oder unlesbaren Output produzieren. Für JSON-Dateien jq . oder python3 -m json.tool verwenden.

Hochperformante Alternative — go-json

Für die überwiegende Mehrheit der Go-Services ist encoding/json schnell genug. Aber wenn JSON-Marshalling im Profiler auftaucht — häufig bei hochdurchsatzstarken REST-APIs oder Services, die bei jeder Anfrage große strukturierte Log-Zeilen ausgeben — ist die Bibliothek go-json ein Drop-in-Ersatz, der 3–5× schneller ist und dieselbe API-Oberfläche bietet.

bash — go-json installieren
go get github.com/goccy/go-json
Go — encoding/json durch go-json mit einer Import-Änderung ersetzen
package main

import (
	// Ersetze dies:
	// "encoding/json"

	// Durch dies — identische API, keine weiteren Codeänderungen:
	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: "rechnung.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))
	// Ausgabe ist identisch mit encoding/json — nur die Geschwindigkeit unterscheidet sich
}

github.com/bytedance/sonic ist die schnellste verfügbare Go-JSON-Bibliothek, läuft aber nur auf amd64 und arm64 (sie verwendet JIT-Kompilierung). Man greift auf go-json zurück, wenn ein portabler Drop-in benötigt wird; auf sonic dann, wenn man auf einer bekannten Architektur ist und in einem Hot Path jede Mikrosekunde zählt.

Mit großen JSON-Dateien arbeiten

json.MarshalIndent und json.Indent erfordern beide, dass der gesamte Payload im Speicher liegt. Für Dateien über 100 MB — Daten-Exports, Audit-Logs, Kafka-Consumer-Payloads — sollte json.Decoder verwendet werden, um den Input zu streamen und Datensätze einzeln zu verarbeiten.

Großes JSON-Array mit json.Decoder streamen

Go — großes JSON-Array streamen ohne vollständiges Laden in den Speicher
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)

	// Öffnendes '[' lesen
	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)
		}
		// Ein Event nach dem anderen verarbeiten — konstanter Speicherverbrauch
		if event.Severity == "error" {
			fmt.Printf("[ERROR] %s: %s (%dms)
", event.UserID, event.Action, event.DurationMs)
		}
		processed++
	}
	fmt.Printf("%d Audit-Events verarbeitet
", processed)
	return nil
}

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

NDJSON — Ein JSON-Objekt pro Zeile

Go — NDJSON-Log-Stream zeilenweise verarbeiten
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 pro Zeile

	for scanner.Scan() {
		var line LogLine
		if err := json.Unmarshal(scanner.Bytes(), &line); err != nil {
			continue // fehlerhafte Zeilen überspringen
		}
		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)
	}
}
Hinweis:Auf Streaming umsteigen, wenn die Datei 100 MB überschreitet oder unbegrenzte Streams verarbeitet werden (Kafka-Consumer, Log-Pipelines, S3-Object-Reads). Das Laden einer 500-MB-JSON-Datei mit os.ReadFile alloziert den gesamten Buffer auf dem Heap, erzeugt GC-Druck und kann auf speicherbeschränkten Containern zu OOM führen.

Häufige Fehler

Den Fehler-Rückgabewert von MarshalIndent ignorieren

Problem: Den Fehler mit _ zu verwerfen bedeutet, dass ein nicht serialisierbarer Wert (ein Channel, eine Funktion, eine komplexe Zahl) stillschweigend nil-Output produziert oder weiter unten beim Aufruf von string(nil) zu einem Panic führt.

Lösung: Den Fehler immer prüfen. Wenn ein Typ marshalliert wird, der immer erfolgreich sein sollte, ist ein Panic mit log.Fatalf besser als stiller Datenverlust.

Before · Go
After · Go
data, _ := json.MarshalIndent(payload, "", "  ")
fmt.Println(string(data)) // leerer String wenn Marshal fehlschlug
data, err := json.MarshalIndent(payload, "", "  ")
if err != nil {
    log.Fatalf("marshal payload: %v", err)
}
fmt.Println(string(data))
fmt.Println statt os.Stdout.Write für binäre Ausgabe verwenden

Problem: fmt.Println(string(data)) hängt ein Zeilenumbruchzeichen nach dem JSON an, was Pipelines korrumpiert, die die Ausgabe als rohe Bytes behandeln — zum Beispiel beim Pipen zu jq oder beim Schreiben in ein binäres Protokoll.

Lösung: os.Stdout.Write(data) für binär-saubere Ausgabe verwenden. Wenn ein abschließender Zeilenumbruch für die menschliche Darstellung benötigt wird, explizit anhängen.

Before · Go
After · Go
data, _ := json.MarshalIndent(cfg, "", "  ")
fmt.Println(string(data)) // fügt einen zusätzlichen Zeilenumbruch am Ende hinzu
data, _ := json.MarshalIndent(cfg, "", "  ")
os.Stdout.Write(data)
os.Stdout.Write([]byte("
")) // expliziter Zeilenumbruch nur wenn nötig
omitempty bei Pointer-Feldern vergessen

Problem: Ohne omitempty serialisiert ein nil-*string- oder *int-Pointer als "field": null. Das legt interne Feldnamen offen und kann strenge JSON-Schema-Validatoren auf der Consumer-Seite brechen.

Lösung: omitempty zu Pointer-Feldern hinzufügen, die in der Ausgabe fehlen (nicht null) sein sollen. Ein nil-*T mit omitempty erzeugt überhaupt keinen Schlüssel im JSON.

Before · Go
After · Go
type WebhookPayload struct {
    EventID   string  `json:"event_id"`
    ErrorMsg  *string `json:"error_msg"`  // erscheint als null wenn nil
}
// {"event_id":"evt_3c7f","error_msg":null}
type WebhookPayload struct {
    EventID   string  `json:"event_id"`
    ErrorMsg  *string `json:"error_msg,omitempty"`  // weggelassen wenn nil
}
// {"event_id":"evt_3c7f"}
map[string]interface{} statt Structs verwenden

Problem: Das Unmarshallen in map[string]any verliert Typinformationen, erfordert manuelle Type-Assertions und erzeugt nicht-deterministische Schlüsselreihenfolge — was JSON-Diffs und Log-Vergleiche schwieriger macht.

Lösung: Ein Struct mit korrekten json-Tags definieren. Structs sind typsicher, marshallen schneller, erzeugen deterministische Feldreihenfolge entsprechend der Struct-Definition und machen den Code selbstdokumentierend.

Before · Go
After · Go
var result map[string]any
json.Unmarshal(body, &result)
port := result["port"].(float64) // Type-Assertion erforderlich, Panic bei falschem Typ
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 // typisiert, sicher, schnell

encoding/json vs. Alternativen — Kurzvergleich

Methode
Formatierte Ausgabe
Gültiges JSON
Eigene Typen
Streaming
Installation nötig
json.MarshalIndent
✓ via MarshalJSON
Nein (stdlib)
json.Indent
N/A (bytes only)
Nein (stdlib)
json.Encoder
✗ (compact)
✓ via MarshalJSON
Nein (stdlib)
go-json
go get
sonic
go get (amd64/arm64)
jq (CLI)
N/A
System-Installation

json.MarshalIndent verwenden, wenn die Struct-Definition kontrolliert wird und formatierte Ausgabe benötigt wird — Konfigurationsdateien, Debug-Logging, Test-Fixtures und API-Response-Logging. json.Indent verwenden, wenn bereits rohe Bytes vorliegen und nur Leerraum hinzugefügt werden muss, ohne einen Umweg über Go-Typen. Zu go-json oder sonic wechseln erst, nachdem das Profiling bestätigt hat, dass JSON-Marshalling ein messbarer Engpass ist — für die meisten Services ist die Standardbibliothek mehr als ausreichend.

Häufig gestellte Fragen

Wie gibt man JSON in Go formatiert aus?

Mit json.MarshalIndent(v, "", "\t") aus dem Paket encoding/json — das zweite Argument ist ein zeilenweises Präfix (meist leer) und das dritte die Einrückung pro Ebene. "\t" für Tabs oder " " für zwei Leerzeichen. Eine externe Bibliothek ist nicht erforderlich; encoding/json gehört zur Go-Standardbibliothek.

Go
package main

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

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

Was ist der Unterschied zwischen json.Marshal und json.MarshalIndent?

json.Marshal erzeugt kompaktes einzeiliges JSON ohne Leerzeichen — ideal für die Netzwerkübertragung, wo jedes Byte zählt. json.MarshalIndent akzeptiert zwei zusätzliche String-Parameter (prefix und indent) und erzeugt eingerückte, lesbare Ausgabe. Beide Funktionen akzeptieren dieselben Werttypen und geben ([]byte, error) zurück. Der einzige Mehraufwand von MarshalIndent sind etwas mehr Ausgabe-Bytes und eine vernachlässigbare CPU-Last für das Einfügen von Leerzeichen.

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

Wie formatiere ich ein JSON-[]byte ohne das Struct zu unmarshallen?

Mit json.Indent(&buf, src, "", "\t"). Diese Funktion nimmt ein vorhandenes []byte mit JSON und schreibt die eingerückte Version in einen bytes.Buffer — ohne Struct-Definition, ohne Type-Assertion, ohne Umweg über Go-Typen. Es ist die schnellste Option, wenn bereits rohe JSON-Bytes vorliegen, etwa aus einem HTTP-Response-Body oder einer Datenbankspalte.

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

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

Warum gibt json.MarshalIndent einen Fehler zurück?

encoding/json gibt einen Fehler zurück, wenn der Wert nicht als JSON dargestellt werden kann. Die häufigsten Ursachen: Ein Channel, eine Funktion oder eine komplexe Zahl wird marshalliert (diese haben kein JSON-Äquivalent); ein Struct, das MarshalJSON() implementiert und einen Fehler zurückgibt; oder eine Map mit Nicht-String-Schlüsseln. Wichtig: Das Marshallieren eines Structs mit einem nicht exportierten oder nil-Pointer-Feld verursacht keinen Fehler — diese werden einfach weggelassen.

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

// Dies gibt einen Fehler — Channels sind nicht JSON-serialisierbar
ch := make(chan int)
_, err := json.MarshalIndent(ch, "", "	")
fmt.Println(err)
// json: unsupported type: chan int

// Dies ist in Ordnung — nil-Pointer-Felder mit omitempty werden stillschweigend weggelassen
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 weggelassen, kein Fehler

Wie schließe ich ein Feld von der JSON-Ausgabe in Go aus?

Es gibt drei Möglichkeiten. Erstens: Den Struct-Tag json:"-" verwenden — das Feld wird unabhängig von seinem Wert immer ausgeschlossen. Zweitens: omitempty verwenden — das Feld wird nur ausgeschlossen, wenn es den Nullwert seines Typs enthält (nil-Pointer, leerer String, 0, false). Drittens: Nicht exportierte (kleingeschriebene) Felder werden von encoding/json automatisch ausgeschlossen, ohne dass ein Tag benötigt wird.

Go
type Zahlungsmethode struct {
	ID           string  `json:"id"`
	Last4        string  `json:"last4"`
	AblaufMonat  int     `json:"expiry_month"`
	AblaufJahr   int     `json:"expiry_year"`
	CVV          string  `json:"-"`                    // immer ausgeschlossen
	Rechnungsname string  `json:"billing_name,omitempty"` // ausgeschlossen wenn leer
	internalRef  string                                 // nicht exportiert — auto ausgeschlossen
}

pm := Zahlungsmethode{
	ID: "pm_9f3a", Last4: "4242",
	AblaufMonat: 12, AblaufJahr: 2028,
	CVV: "123", internalRef: "stripe:pm_9f3a",
}
data, _ := json.MarshalIndent(pm, "", "  ")
// CVV, Rechnungsname (leer) und internalRef erscheinen nicht in der Ausgabe

Wie verarbeite ich time.Time beim JSON-Marshalling?

encoding/json marshalliert time.Time standardmäßig im Format RFC3339Nano (z. B. "2026-03-10T14:22:00Z"), das mit ISO 8601 kompatibel ist. Wenn ein anderes Format benötigt wird — etwa Unix-Epoch-Integer für eine Legacy-API oder ein benutzerdefinierter Datumsstring — implementiert man MarshalJSON() auf einem Wrapper-Typ, der time.Time einbettet und das gewünschte Format zurückgibt.

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

// Standardverhalten — RFC3339Nano, kein eigener Code nötig
type AuditEvent struct {
	Aktion     string    `json:"action"`
	EingetratenAm time.Time `json:"occurred_at"`
}

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

// Benutzerdefiniert: Unix-Zeitstempel als Integer
type UnixTime struct{ time.Time }

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

Verwandte Tools

Auch verfügbar 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üllerTechnischer Prüfer

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.