Pretty-print JSON in Go

json.MarshalIndent(v, "", " ")

json.MarshalIndent(v, "", " ") encodes a Go value as JSON with each element on its own line, indented two spaces per level. The second argument is a prefix for every new line, usually empty; the third is one level of indent.

type User struct {
	Name    string   `json:"name"`
	ID      uint64   `json:"id"`
	Tags    []string `json:"tags"`
	Active  bool     `json:"active"`
	Manager *string  `json:"manager"`
}

u := User{Name: "café", ID: 12345678901234567890, Tags: []string{"api", "ops"}, Active: true}
out, err := json.MarshalIndent(u, "", "  ")
if err != nil {
	log.Fatal(err)
}
fmt.Println(string(out))
{
  "name": "café",
  "id": 12345678901234567890,
  "tags": [
    "api",
    "ops"
  ],
  "active": true,
  "manager": null
}

Struct fields come out in declaration order, and the nil pointer is null. The formatter below prints the same for that JSON text with 2 spaces.

JSON you already have: json.Indent

For bytes from a file or an HTTP body, don’t decode and re-encode. json.Indent reformats the bytes themselves, so every number and string escape stays exactly as written, and it returns an error for invalid input:

var buf bytes.Buffer
if err := json.Indent(&buf, body, "", "  "); err != nil {
	log.Fatal(err) // for {"port":8080,}: invalid character '}' looking for beginning of object key string
}
fmt.Println(buf.String())

Run on the text above, it prints the same output. json.Compact minifies the same way, and json.Valid only checks.

Decoding into any loses things

var v any
json.Unmarshal([]byte(`{"price":1.10,"id":12345678901234567890,"exp":1e5,"b":1,"a":2}`), &v)
out, _ := json.MarshalIndent(v, "", "  ")
fmt.Println(string(out))
{
  "a": 2,
  "b": 1,
  "exp": 100000,
  "id": 12345678901234567000,
  "price": 1.1
}

Every number becomes a float64, exact only up to 2^53, and a map’s keys are written sorted. A number too big for a float64, such as 1e1000, makes Unmarshal fail with json: cannot unmarshal number 1e1000 into Go value of type float64. A Decoder with UseNumber keeps each number’s text as a json.Number:

dec := json.NewDecoder(bytes.NewReader(body))
dec.UseNumber()
var v any
if err := dec.Decode(&v); err != nil {
	log.Fatal(err)
}

Re-encoded, that gives "exp": 1e5, "id": 12345678901234567890 and "price": 1.10, with the keys still sorted: what the formatter below shows with Sort keys on.

HTML escaping

Marshal and MarshalIndent escape <, > and & so the output is safe inside an HTML <script> tag: "<b>Tom & Jerry</b>" comes out as "\u003cb\u003eTom \u0026 Jerry\u003c/b\u003e". To keep them readable, use an Encoder:

enc := json.NewEncoder(os.Stdout)
enc.SetEscapeHTML(false)
enc.SetIndent("", "  ")
enc.Encode(map[string]string{"html": "<b>Tom & Jerry</b>"})
{
  "html": "<b>Tom & Jerry</b>"
}

Encode ends the output with a newline, which MarshalIndent doesn’t. json.Indent never escapes: it copies strings as they are.

Try it in the JSON Formatter & Validator

Result
{
  "name": "café",
  "id": 12345678901234567890,
  "tags": [
    "api",
    "ops"
  ],
  "active": true,
  "manager": null
}

5 keys, nesting 2, 92 bytes in → 126 bytes out

Runs in your browser. Nothing you enter is sent anywhere.