How to Pretty-Print JSON in Go

Use the `encoding/json` package's `MarshalIndent` function to format JSON with indentation and newlines, or pipe the output through the `jq` command-line tool for quick terminal formatting.

Debugging minified JSON

You are debugging an API response and the terminal spits out a single line of characters. {"user":{"id":123,"prefs":{"theme":"dark","lang":"en"},"active":true}}. You need to verify the theme value, but the structure is compressed into a blob. Copying the string into a browser extension breaks your flow. You want the hierarchy visible right where you are working.

Pretty-printing JSON solves this by inserting newlines and indentation. The data stays identical; only the presentation changes. JSON parsers ignore whitespace between tokens. {"a":1} and { "a" : 1 } parse to the exact same result. Pretty-printing adds bytes that machines discard. The cost is storage and bandwidth. The benefit is human readability.

Go provides two standard tools for this. json.MarshalIndent formats a Go value into pretty JSON. json.Indent reformats existing JSON bytes. Both live in the encoding/json package.

Whitespace is free for machines

JSON parsers ignore whitespace between tokens. {"a":1} and { "a" : 1 } parse to the exact same result. Pretty-printing adds bytes that machines discard. The cost is storage and bandwidth. The benefit is human readability.

In Go, the standard library treats formatting as a separate concern from encoding. You can marshal data to compact JSON for production and switch to indented output for debugging without changing the data structure. The function signatures make this explicit.

MarshalIndent formats Go values

Here is the standard way to format JSON in Go: marshal a value with indentation.

package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	// Map holds the data structure we want to format.
	data := map[string]interface{}{
		"name": "Alice",
		"age":  30,
	}

	// MarshalIndent returns a byte slice with newlines and spaces.
	// The empty string is the prefix for each line.
	// Two spaces define the indentation level.
	pretty, err := json.MarshalIndent(data, "", "  ")
	if err != nil {
		// MarshalIndent only fails if the data contains unmarshalable types.
		panic(err)
	}

	// Convert bytes to string for display.
	fmt.Println(string(pretty))
}

MarshalIndent takes three arguments. The first is the value to encode. The second is a prefix string added to the beginning of every line. The third is the indent string repeated for each nesting level. The function returns a byte slice and an error.

The prefix argument is rarely used. You might pass a marker string if you need to align multiple JSON blocks in a report, or add a character to every line for a line-oriented parser. In most cases, pass an empty string. The indent argument controls the visual structure. Two spaces are common. Tabs work too. The choice is stylistic.

The function uses reflection to walk the value. It respects struct tags just like Marshal. Fields tagged with json:"-" disappear. Fields tagged with json:",omitempty" vanish when empty. The indentation logic applies to whatever structure the tags produce.

Indentation is for humans. Prefix is for alignment. Don't mix them up.

Reformatting existing JSON with Indent

Sometimes you already have JSON bytes and want to format them. Unmarshaling to a Go value and marshaling back is wasteful. It loses type information and doubles the memory work. json.Indent reformats JSON bytes directly.

Here is how to pretty-print a JSON string without converting it to a Go type.

package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	// Raw JSON bytes from an external source.
	raw := []byte(`{"name":"Bob","active":true}`)

	// Indent takes input bytes, prefix, and indent.
	// It validates the JSON structure while formatting.
	// The first argument is the destination buffer.
	// Passing nil allocates a new slice.
	formatted, err := json.Indent(nil, raw, "", "  ")
	if err != nil {
		// Indent returns an error if the input is malformed.
		panic(err)
	}

	fmt.Println(string(formatted))
}

json.Indent parses the input bytes and inserts whitespace. It validates the JSON as it goes. If the input is malformed, it returns an error. This makes Indent useful for validation as well as formatting.

The first argument is a destination buffer. Passing nil tells the function to allocate a new byte slice. If you have a pre-allocated buffer, you can pass it to reuse memory. The function returns the filled buffer.

Buffer reuse is a performance win. When you call json.Indent in a loop, passing nil forces a new allocation every iteration. You can pass a pre-allocated byte slice as the first argument. The function appends the formatted JSON to the slice and returns the extended slice. This reuses memory across calls. Clear the slice between iterations to avoid growing the buffer indefinitely.

// Reuse a buffer to avoid allocations in a loop.
buf := make([]byte, 0, 1024)

for _, raw := range payloads {
	// Indent appends to buf and returns the extended slice.
	// This reuses the underlying array memory.
	result, err := json.Indent(buf, raw, "", "  ")
	if err != nil {
		// Handle error.
	}

	// Process result.
	// ...

	// Reset the slice length to zero for the next iteration.
	// The capacity remains, so the memory is reused.
	buf = result[:0]
}

MarshalIndent converts a Go value to JSON. Indent takes JSON bytes and reformats them. Pick the tool that matches your input.

Real-world usage in HTTP handlers

In production, you rarely print JSON to stdout. You return it from an API or log it. Pretty-printing is useful for local debugging endpoints. Here is how to format JSON in an HTTP response.

package main

import (
	"encoding/json"
	"net/http"
)

// Response represents the API payload structure.
type Response struct {
	Status string `json:"status"`
	Data   User   `json:"data"`
}

// User holds user details.
type User struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

// handleDebug returns pretty-printed JSON for local testing.
func handleDebug(w http.ResponseWriter, r *http.Request) {
	// Build the response object.
	resp := Response{
		Status: "ok",
		Data:   User{ID: 42, Name: "Bob"},
	}

	// MarshalIndent formats the output for human readability.
	body, err := json.MarshalIndent(resp, "", "\t")
	if err != nil {
		// Return 500 if marshaling fails, though unlikely with basic types.
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}

	// Set content type so the browser renders it correctly.
	w.Header().Set("Content-Type", "application/json")
	w.Write(body)
}

The handler builds a struct and marshals it with tabs. The Content-Type header is crucial. Browsers use the header to decide how to render the response. Without application/json, the browser might display minified text even if the JSON is indented. Setting the header lets the browser format the output in its own viewer.

Go struct tags control the JSON keys. MarshalIndent uses the same rules as Marshal. If you change a tag, the indentation follows the new structure. The boilerplate if err != nil { return err } pattern is standard. The community accepts the verbosity because it makes error handling explicit.

The browser needs the header. The developer needs the indent. Set both.

Pitfalls and performance

MarshalIndent allocates a new byte slice. It copies the data. For large payloads, this doubles memory usage temporarily. The function walks the entire value tree. Deep nesting increases the work.

Reflection adds overhead. MarshalIndent uses reflection to walk the value tree. It inspects types at runtime. This is slower than code-generated serialization. For small payloads, the difference is negligible. For large payloads or high-throughput loops, the cost adds up. MarshalIndent is also slower than Marshal because it calculates indentation and inserts whitespace. If you need speed, stick to Marshal and format only for debugging.

If you pass a type that JSON cannot encode, the function returns an error. Channels, functions, and complex numbers are unsupported. The compiler catches type mismatches at compile time, but runtime errors occur if you pass a dynamic value. The compiler rejects invalid types with json: unsupported type: chan int. This error appears at runtime if you use interface{} or reflection to pass data. Avoid passing channels or functions to marshaling functions.

json.Indent validates the input. Malformed JSON triggers an error. The error message points to the problem. You might see invalid character 'x' looking for beginning of object key string if the input has syntax errors. Use Indent to validate JSON strings from external sources.

Circular references cause infinite loops. The standard library detects cycles and returns an error. The error message mentions MarshalIndent or Marshal depending on the function. Design your data structures to avoid cycles. Use pointers carefully.

Whitespace costs bytes. Only pay for it when a human is reading.

Decision matrix

Use json.MarshalIndent when you have a Go value and need formatted JSON for logs, debug output, or local API responses.

Use json.Marshal when performance matters and the output goes to a machine, such as a production API response or a file store.

Use json.Indent when you already have JSON bytes and want to reformat them without unmarshaling, or when you need to validate JSON syntax.

Use jq in the terminal when you are debugging a stream of JSON from a command or script and don't want to modify code.

Use fmt.Printf("%+v", data) when you just need a quick dump of a struct and don't care about valid JSON syntax.

Where to go next