JSON Formatter Go β MarshalIndent() Guide
Use the free online JSON Formatter & Beautifier directly in your browser β no install required.
Try JSON Formatter & Beautifier Online βWhen I'm working on a Go microservice and need to inspect an API response or a config file, compact JSON is the first obstacle β a single line with hundreds of nested fields tells me almost nothing at a glance. To format JSON in Go, the standard library gives you everything you need: json.MarshalIndent is built into encoding/json, ships with every Go installation, and requires zero third-party dependencies. This guide covers the full picture: struct tags, custom MarshalJSON() implementations, json.Indent for re-formatting raw bytes, streaming large files with json.Decoder, and when to reach for go-json for high-throughput paths, plus CLI one-liners for quick formatting in the terminal. All examples use Go 1.21+.
- βjson.MarshalIndent(v, "", "\t") is standard library β zero dependencies, ships with every Go install.
- βStruct tags json:"field_name,omitempty" control serialization keys and omit zero-value fields from output.
- βImplement MarshalJSON() on any type to fully control its JSON representation.
- βjson.Indent() re-formats already-marshaled []byte without re-parsing the struct β faster for raw bytes.
- βFor large files (>100 MB): use json.Decoder with Token() to stream without loading all into memory.
- βgo-json is a drop-in replacement 3β5Γ faster than encoding/json for high-throughput APIs.
What is JSON Formatting?
JSON formatting β also called pretty-printing β transforms a compact, minified JSON string into a human-readable layout with consistent indentation and line breaks. The underlying data is identical; only the whitespace changes. Compact JSON is optimal for network transfer where every byte matters; formatted JSON is optimal for debugging, code review, log inspection, and config file authoring. Go's encoding/json package handles both modes with a single function call β toggle between compact and indented output by choosing between json.Marshal and json.MarshalIndent.
{"service":"payments","port":8443,"workers":4}{
"service": "payments",
"port": 8443,
"workers": 4
}json.MarshalIndent() β The Standard Library Approach
json.MarshalIndent lives in the encoding/json package, which is part of the Go standard library β no go get required. Its signature is MarshalIndent(v any, prefix, indent string) ([]byte, error): the prefix string is prepended to every output line (almost always left empty), and indent is repeated once per nesting level. Pass "\t" for tabs or " " for two spaces.
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"
// ]
// }The choice between tabs and spaces is mostly a team convention. Many Go projects prefer tabs because gofmt (which formats Go source code) uses tabs. Two-space or four-space indentation is common when the JSON is destined for a JavaScript or Python consumer. Here is the same struct with both indent styles side by side:
// Tab indent β preferred in Go-native tooling
data, _ := json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }
// Two-space indent β common for APIs with JS/Python consumers
data, _ = json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }
// Four-space indent
data, _ = json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }json.Marshal(v) when you need compact output β network payloads, cache values, or any path where binary size matters. It takes the same value argument and has the same error semantics, but produces single-line JSON without any whitespace.Struct Tags β Controlling Field Names and Omitempty
Go struct tags are string literals placed after field declarations that tellencoding/jsonhow to serialize each field. There are three key directives: json:"name" renames the field in output, omitempty omits the field when it holds the zero value for its type, and json:"-" excludes the field entirely β useful for passwords, internal identifiers, or fields that must never leave the service boundary.
type UserProfile struct {
ID string `json:"id"`
Email string `json:"email"`
DisplayName string `json:"display_name,omitempty"` // omit if empty string
AvatarURL *string `json:"avatar_url,omitempty"` // omit if nil pointer
IsAdmin bool `json:"is_admin,omitempty"` // omit if false
passwordHash string // unexported β auto excluded
}
// User with all optional fields populated
full := UserProfile{
ID: "usr_7b3c", Email: "ops@example.com",
DisplayName: "Ops Team", IsAdmin: true,
}
// {
// "id": "usr_7b3c",
// "email": "ops@example.com",
// "display_name": "Ops Team",
// "is_admin": true
// }
// User with no optional fields β they are omitted entirely
minimal := UserProfile{ID: "usr_2a91", Email: "dev@example.com"}
// {
// "id": "usr_2a91",
// "email": "dev@example.com"
// }The json:"-" tag is the right choice for fields that must be unconditionally excluded regardless of their value β typically secrets, internal tracking fields, or data that is correct in memory but must not be serialized to any external system.
type AuthToken struct {
TokenID string `json:"token_id"`
Subject string `json:"sub"`
IssuedAt int64 `json:"iat"`
ExpiresAt int64 `json:"exp"`
SigningKey []byte `json:"-"` // never serialized
RefreshToken string `json:"-"` // never serialized
}
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 and RefreshToken never appearencoding/json regardless of any tags. You do not need to add json:"-" to unexported fields β the exclusion is automatic and cannot be overridden.Custom MarshalJSON() β Handling Non-Standard Types
Any Go type can implement the json.Marshaler interface by defining a MarshalJSON() ([]byte, error) method. When encoding/json encounters such a type, it calls the method instead of its default reflection-based marshaling. This is the canonical Go pattern for domain types that need a specific wire representation β monetary values, status enums, custom time formats, or any type that stores data differently from how it should be serialized.
Custom Type β Money with Cents-to-Decimal Conversion
package main
import (
"encoding/json"
"fmt"
"log"
)
type Money struct {
Amount int64 // stored in cents to avoid floating-point drift
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: "USD"},
Tax: Money{Amount: 1592, Currency: "USD"},
Total: Money{Amount: 21492, Currency: "USD"},
}
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": "USD", "display": "USD 199.00" },
// "tax": { "amount": 15.92, "currency": "USD", "display": "USD 15.92" },
// "total": { "amount": 214.92, "currency": "USD", "display": "USD 214.92" }
// }Status Enum β String Representation
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"
// }MarshalJSON() and UnmarshalJSON() together. If you only implement marshaling, round-tripping the type through JSON (serialize β store β deserialize) will silently lose structure or return the wrong type. The pair forms a contract that the type can survive a JSON round-trip.UUID β Serialize as String
Go's standard library has no UUID type. The most common choice is github.com/google/uuid, which already implements MarshalJSON() and serializes as a quoted RFC 4122 string. If you use a raw [16]byte or a custom ID type, implement the interface yourself to avoid base64-encoded binary blobs in your JSON output.
import (
"encoding/json"
"fmt"
"github.com/google/uuid"
)
type AuditEvent struct {
EventID uuid.UUID `json:"event_id"` // serializes as "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"
// }[16]byte without a custom type, encoding/json encodes them as a base64 string β e.g. "VQ6EAOKbQdSnFkRmVUQAAA==". Always use a proper UUID type or implement MarshalJSON() to emit the canonical hyphenated string format.json.MarshalIndent() Parameters Reference
The function signature is func MarshalIndent(v any, prefix, indent string) ([]byte, error). Both prefix and indent are string literals β there is no numeric shorthand like Python's indent=4.
Common struct tag options:
json.Indent() β Re-formatting Existing JSON Bytes
When you already have a []byte of JSON β say from an HTTP response body, a Postgres jsonb column, or a file read with os.ReadFile β you do not need to define a struct and unmarshal before you can pretty-print. json.Indent directly reformats the raw bytes by writing indented output into a bytes.Buffer.
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
)
func main() {
// Simulating a raw JSON payload from an upstream 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
// }A common pattern I use in microservices is to call json.Indent before writing to structured logs β it adds negligible overhead and makes log entries far easier to read during an incident. The function is particularly useful for logging HTTP responses, pretty-printing stored JSON strings, and format-on-read pipelines where the struct definition is unavailable.
func logResponse(logger *slog.Logger, statusCode int, body []byte) {
var pretty bytes.Buffer
if err := json.Indent(&pretty, body, "", " "); err != nil {
// Body is not valid JSON β log raw
logger.Debug("upstream response", "status", statusCode, "body", string(body))
return
}
logger.Debug("upstream response", "status", statusCode, "body", pretty.String())
}json.Indent does NOT fully validate the JSON beyond what is structurally required to insert whitespace. For full syntax validation, call json.Valid(data) first and handle the false case before attempting to indent.Format JSON from a File and HTTP Response
Two of the most common real-world scenarios in Go services are formatting JSON read from a file on disk (config files, fixture data, migration seeds) and pretty-printing HTTP response bodies for debug logging or test assertions. Both follow the same pattern: read bytes, call json.Indent or unmarshal then json.MarshalIndent, write back or log.
Read File β Format β Write Back
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 formatted successfully")
}HTTP Response β Decode β Pretty-Print for Debug Logging
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
// }Raw Response Body β json.Indent (No Struct Required)
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())
}Pretty Print JSON from an HTTP Response in Go
The two approaches above cover the most common cases: decode into a typed struct then call json.MarshalIndent (best when you need to validate or inspect fields), or read the raw body bytes with io.ReadAll and call json.Indent directly (best for quick debug logging when you do not have a struct definition handy). The raw-bytes approach is simpler but does not give you Go type safety or field access β it is purely a display transformation. Both approaches handle large response bodies correctly as long as the full body fits in memory.
// Pattern A: typed decode β MarshalIndent // Use when you need to inspect or validate specific fields var result map[string]any json.NewDecoder(resp.Body).Decode(&result) pretty, _ := json.MarshalIndent(result, "", " ") fmt.Println(string(pretty)) // Pattern B: raw bytes β json.Indent // Use for quick debug logging β no struct definition required body, _ := io.ReadAll(resp.Body) var buf bytes.Buffer json.Indent(&buf, body, "", " ") fmt.Println(buf.String())
Command-Line JSON Formatting in Go Projects
Sometimes you need to format a JSON payload right in the terminal without writing a Go program. These one-liners are the ones I keep in muscle memory during development and incident response.
echo '{"service":"payments","port":8443,"workers":4}' | python3 -m json.tool
# {
# "service": "payments",
# "port": 8443,
# "workers": 4
# }# Format only cat api-response.json | jq . # Extract a nested field cat api-response.json | jq '.checks.database' # Filter an array cat audit-log.json | jq '.[] | select(.severity == "error")'
# main.go: reads stdin, formats, writes stdout
cat <<'EOF' > /tmp/fmt.go
package main
import ("bytes";"encoding/json";"io";"os")
func main() {
b,_:=io.ReadAll(os.Stdin)
var buf bytes.Buffer
json.Indent(&buf,b,""," ")
os.Stdout.Write(buf.Bytes())
}
EOF
echo '{"port":8080,"debug":true}' | go run /tmp/fmt.gogofmt formats Go source code, not JSON. Do not pipe JSON files through gofmt β it will either error or produce unrecognizable output. Use jq . or python3 -m json.tool for JSON files.High-Performance Alternative β go-json
For the vast majority of Go services, encoding/json is fast enough. But if JSON marshaling shows up in your profiler β common in high-throughput REST APIs or services that emit large structured log lines on every request β the go-json library is a drop-in replacement that is 3β5Γ faster with identical API surface.
go get github.com/goccy/go-json
package main
import (
// Replace this:
// "encoding/json"
// With this β identical API, no other code changes:
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))
// Output is identical to encoding/json β only speed differs
}github.com/bytedance/sonic is the fastest Go JSON library available, but it only runs on amd64 and arm64 (it uses JIT compilation). Use go-json when you need a portable drop-in; reach for sonic when you are on a known architecture and need every microsecond in a hot path.
Working with Large JSON Files
json.MarshalIndent and json.Indent both require the entire payload to be in memory. For files above 100 MB β data exports, audit logs, Kafka consumer payloads β use json.Decoder to stream the input and process records one at a time.
Streaming a Large JSON Array with json.Decoder
package main
import (
"encoding/json"
"fmt"
"log"
"os"
)
type AuditEvent struct {
RequestID string `json:"request_id"`
UserID string `json:"user_id"`
Action string `json:"action"`
Severity string `json:"severity"`
DurationMs int `json:"duration_ms"`
}
func processAuditLog(path string) error {
file, err := os.Open(path)
if err != nil {
return fmt.Errorf("open %s: %w", path, err)
}
defer file.Close()
dec := json.NewDecoder(file)
// Read the opening '['
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)
}
// Process one event at a time β constant memory usage
if event.Severity == "error" {
fmt.Printf("[ERROR] %s: %s (%dms)
", event.UserID, event.Action, event.DurationMs)
}
processed++
}
fmt.Printf("Processed %d audit events
", processed)
return nil
}
func main() {
if err := processAuditLog("audit-2026-03.json"); err != nil {
log.Fatalf("process audit log: %v", err)
}
}NDJSON β One JSON Object Per Line
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 line
for scanner.Scan() {
var line LogLine
if err := json.Unmarshal(scanner.Bytes(), &line); err != nil {
continue // skip malformed lines
}
if line.Level == "error" {
fmt.Printf("%s [%s] %s trace=%s
",
line.Timestamp, line.Service, line.Message, line.TraceID)
}
}
if err := scanner.Err(); err != nil {
log.Fatalf("scan: %v", err)
}
}os.ReadFile will allocate that entire buffer on the heap, trigger GC pressure, and may cause OOM on memory-constrained containers.Common Mistakes
Problem: Discarding the error with _ means a non-serializable value (a channel, a function, a complex number) silently produces nil output or panics downstream when you call string(nil).
Fix: Always check the error. If you are marshaling a type that should always succeed, a panicking log.Fatalf is better than silent data loss.
data, _ := json.MarshalIndent(payload, "", " ") fmt.Println(string(data)) // empty string if marshal failed
data, err := json.MarshalIndent(payload, "", " ")
if err != nil {
log.Fatalf("marshal payload: %v", err)
}
fmt.Println(string(data))Problem: fmt.Println(string(data)) appends a newline character after the JSON, which corrupts pipelines that treat the output as raw bytes β for example, when piping to jq or writing to a binary protocol.
Fix: Use os.Stdout.Write(data) for binary-clean output. If you need a trailing newline for human display, append it explicitly.
data, _ := json.MarshalIndent(cfg, "", " ") fmt.Println(string(data)) // adds an extra newline at the end
data, _ := json.MarshalIndent(cfg, "", " ")
os.Stdout.Write(data)
os.Stdout.Write([]byte("
")) // explicit newline only when neededProblem: Without omitempty, a nil *string or *int pointer serializes as "field": null. This exposes internal field names and can break strict JSON schema validators on the consumer side.
Fix: Add omitempty to pointer fields you want absent (not null) in the output. A nil *T with omitempty produces no key at all in the JSON.
type WebhookPayload struct {
EventID string `json:"event_id"`
ErrorMsg *string `json:"error_msg"` // appears as null when nil
}
// {"event_id":"evt_3c7f","error_msg":null}type WebhookPayload struct {
EventID string `json:"event_id"`
ErrorMsg *string `json:"error_msg,omitempty"` // omitted when nil
}
// {"event_id":"evt_3c7f"}Problem: Unmarshaling into map[string]any loses type information, requires manual type assertions, and produces non-deterministic key order β making JSON diffs and log comparisons harder.
Fix: Define a struct with proper json tags. Structs are type-safe, marshal faster, produce deterministic field order matching the struct definition, and make the code self-documenting.
var result map[string]any json.Unmarshal(body, &result) port := result["port"].(float64) // type assertion required, panics if wrong 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 // typed, safe, fastencoding/json vs Alternatives β Quick Comparison
Use json.MarshalIndent for any case where you control the struct definition and need formatted output β config files, debug logging, test fixtures, and API response logging. Use json.Indent when you already have raw bytes and just need whitespace added without a round-trip through Go types. Switch to go-json or sonic only after profiling confirms that JSON marshaling is a measurable bottleneck β for most services, the standard library is more than sufficient.
Frequently Asked Questions
How do I pretty print JSON in Go?
Call json.MarshalIndent(v, "", "\t") from the encoding/json package β the second argument is a per-line prefix (usually empty) and the third is the per-level indent. Pass "\t" for tabs or " " for two spaces. No external library is required; encoding/json ships with the Go standard library.
package main
import (
"encoding/json"
"fmt"
"log"
)
func main() {
config := map[string]any{
"service": "payments-api",
"port": 8443,
"region": "us-east-1",
}
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
log.Fatalf("marshal: %v", err)
}
fmt.Println(string(data))
// {
// "port": 8443,
// "region": "us-east-1",
// "service": "payments-api"
// }
}What is the difference between json.Marshal and json.MarshalIndent?
json.Marshal produces compact single-line JSON with no whitespace β ideal for network transfer where every byte counts. json.MarshalIndent takes two extra string parameters (prefix and indent) and produces indented, human-readable output. Both functions accept the same value types and return ([]byte, error). The only cost of MarshalIndent is slightly more output bytes and a negligible amount of extra CPU for inserting whitespace.
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
// }How do I format a JSON []byte without unmarshaling the struct?
Use json.Indent(&buf, src, "", "\t"). This function takes an existing []byte of JSON and writes the indented version into a bytes.Buffer β no struct definition needed, no type assertion, no round-trip through Go types. It is the fastest option when you already have raw JSON bytes, such as from an HTTP response body or a database column.
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
// }Why does json.MarshalIndent return an error?
encoding/json returns an error when the value cannot be represented as JSON. The most common causes are: marshaling a channel, a function, or a complex number (these have no JSON equivalent); a struct that implements MarshalJSON() and returns an error; or a map with non-string keys. Importantly, marshaling a struct with an unexported or nil pointer field does NOT cause an error β those are simply omitted.
import (
"encoding/json"
"fmt"
)
// This will return an error β channels are not JSON-serializable
ch := make(chan int)
_, err := json.MarshalIndent(ch, "", " ")
fmt.Println(err)
// json: unsupported type: chan int
// This is fine β nil pointer fields with omitempty are omitted silently
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 omitted, no errorHow do I exclude a field from JSON output in Go?
There are three ways. First, use the json:"-" struct tag β the field is always excluded regardless of its value. Second, use omitempty β the field is excluded only when it holds the zero value for its type (nil pointer, empty string, 0, false). Third, unexported (lowercase) fields are automatically excluded by encoding/json without any tag needed.
type PaymentMethod struct {
ID string `json:"id"`
Last4 string `json:"last4"`
ExpiryMonth int `json:"expiry_month"`
ExpiryYear int `json:"expiry_year"`
CVV string `json:"-"` // always excluded
BillingName string `json:"billing_name,omitempty"` // excluded if empty
internalRef string // unexported β auto excluded
}
pm := PaymentMethod{
ID: "pm_9f3a", Last4: "4242",
ExpiryMonth: 12, ExpiryYear: 2028,
CVV: "123", internalRef: "stripe:pm_9f3a",
}
data, _ := json.MarshalIndent(pm, "", " ")
// CVV, BillingName (empty), and internalRef do not appear in outputHow do I handle time.Time in JSON marshaling?
encoding/json marshals time.Time to RFC3339Nano format by default (e.g. "2026-03-10T14:22:00Z"), which is ISO 8601 compatible. If you need a different format β such as Unix epoch integers for a legacy API, or a custom date-only string β implement MarshalJSON() on a wrapper type that embeds time.Time and returns the format you need.
import (
"encoding/json"
"fmt"
"time"
)
// Default behavior β RFC3339Nano, no custom code needed
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"
// }
// Custom: Unix timestamp as integer
type UnixTime struct{ time.Time }
func (u UnixTime) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("%d", u.Unix())), nil
}Related Tools
James is a systems engineer and Go enthusiast who focuses on high-performance microservices, command-line tooling, and infrastructure automation. He enjoys the simplicity and explicitness of Go and writes about building fast, reliable backend systems. When not coding he explores distributed systems concepts and contributes to open-source Go libraries.
Tobias is a platform engineer who builds developer tooling and internal infrastructure in Go. He has authored several open-source CLI tools and contributes to the Go toolchain ecosystem. He writes about the cobra and urfave/cli frameworks, cross-platform binary distribution, configuration management, and the patterns that make Go an ideal language for building reliable, self-contained command-line utilities.