How to Marshal (Encode) JSON in Go

Use the `encoding/json` package's `Marshal` function to convert Go data structures into JSON byte slices, or `MarshalIndent` if you need pretty-printed output with indentation.

The struct won't talk JSON until you teach it

You just finished a function that calculates a user's profile. You have a struct with an ID, a name, and an email. You send it to the frontend, and the JavaScript code crashes because it received {ID:1 Name:Alice Email:} instead of {"id":1,"name":"Alice"}. Go doesn't guess what format the outside world wants. You have to tell it explicitly.

Marshaling is the process of converting a Go value into a sequence of bytes. JSON is a text format, so marshaling produces a byte slice containing valid JSON text. Think of the encoding/json package as a translator. You speak Go, with structs, types, and memory layouts. The client speaks JSON, with keys, values, and strings. The marshaler walks your Go value, converts every field, and hands back bytes the client understands.

Minimal example

Here's the simplest way to turn a struct into JSON bytes. Define the struct, add tags to control the output, call Marshal, and handle the error.

package main

import (
	"encoding/json"
	"fmt"
)

// User holds profile data for an API response.
// Tags map Go fields to JSON keys.
type User struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

func main() {
	u := User{ID: 42, Name: "Go"}

	// Marshal converts the struct to a JSON byte slice.
	// It returns an error if the type cannot be encoded.
	data, err := json.Marshal(u)
	if err != nil {
		panic(err)
	}

	// Convert bytes to string only for display.
	// Keep data as []byte for network or file writes.
	fmt.Println(string(data))
}
# output:
{"id":42,"name":"Go"}

The result is compact JSON with no whitespace. Marshal returns []byte, not a string. This matters because JSON is usually written to a network socket or a file. Bytes are efficient for I/O. Converting to a string creates a copy. Keep the data as bytes until you absolutely need a string, like when printing to a console.

How the encoder works

When you call Marshal, the runtime inspects your value using reflection. It walks the struct fields, checks for tags, converts Go types to JSON types, and allocates a new byte slice. The process happens at runtime, not compile time. This means the compiler can't check if your tags are valid or if your struct matches the expected schema. You'll find those bugs when the JSON comes back wrong.

The function accepts any, so you can pass almost anything. If you pass a type the encoder doesn't know how to handle, it returns an error. Channels, functions, and complex numbers are unsupported. The compiler won't catch this because Marshal takes an interface. You'll get a runtime error like json: unsupported type: chan int if you try to encode a channel.

Error handling is mandatory. Marshal can fail on circular references, unsupported types, or custom encoding errors. The community accepts the verbose if err != nil pattern because it makes the failure path visible. Never ignore the error return. If the marshal fails, your program sends garbage or crashes downstream.

Realistic API response

In a real service, you need control over field names, empty values, and formatting. Struct tags let you map Go fields to JSON keys and drop fields when they have zero values.

package main

import (
	"encoding/json"
	"fmt"
)

// APIResponse wraps the payload and status for a JSON endpoint.
// Tags map Go fields to JSON keys and control omission.
type APIResponse struct {
	Status  string `json:"status"`
	Message string `json:"message,omitempty"`
	Data    *User  `json:"data"`
}

type User struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email,omitempty"`
}

func main() {
	// Simulate a response where email is missing.
	resp := APIResponse{
		Status: "ok",
		Data: &User{
			ID:   1,
			Name: "Alice",
			// Email is empty string, so omitempty will drop it.
		},
	}

	// Marshal produces compact JSON for the network.
	body, err := json.Marshal(resp)
	if err != nil {
		panic(err)
	}
	fmt.Println("Network payload:", string(body))
}
# output:
Network payload: {"status":"ok","data":{"id":1,"name":"Alice"}}

Notice Message is missing from the output. The omitempty tag tells the encoder to skip the field if it has a zero value. Zero values depend on the type. An integer zero, a boolean false, an empty string, a nil pointer, or an empty slice all count as empty. If the field is a pointer to a struct, a nil pointer omits the field, but a pointer to an empty struct includes the empty object. This distinction trips up many developers. Check your pointers carefully.

For debugging or logs, you might want formatted JSON. MarshalIndent adds whitespace to make the output readable.

// MarshalIndent creates formatted JSON for debugging or logs.
// It takes a prefix for the first line and an indent string for subsequent lines.
pretty, err := json.MarshalIndent(resp, "", "  ")
if err != nil {
	panic(err)
}
fmt.Println(string(pretty))
# output:
{
  "status": "ok",
  "data": {
    "id": 1,
    "name": "Alice"
  }
}

MarshalIndent is slower than Marshal because it allocates more memory and inserts whitespace. Use it only for human consumption. Never use it for production API responses. The extra bytes waste bandwidth and CPU cycles.

Pitfalls and silent failures

The encoder skips unexported fields silently. If you have id int instead of ID int, it won't appear in the JSON. There is no error. The field just vanishes. This catches everyone eventually. Always capitalize fields you want in JSON. Go's visibility rules apply to reflection too.

Circular references cause infinite recursion. If struct A contains a pointer to struct B, and struct B contains a pointer back to struct A, the encoder walks the cycle forever. The package detects this and returns an error. You'll see json: encoding error: circular reference detected if your data structure has a loop. Break the cycle by using omitempty on one side or restructuring the types.

Custom encoding lets you change how a type appears in JSON. If the default behavior isn't enough, implement the json.Marshaler interface. This is useful for durations, money types, or custom date formats.

// DurationSeconds wraps time.Duration to encode as seconds.
type DurationSeconds time.Duration

// MarshalJSON implements json.Marshaler for custom encoding.
// It converts the duration to a float and returns JSON bytes.
func (d DurationSeconds) MarshalJSON() ([]byte, error) {
	// Convert to seconds and return as a JSON number.
	secs := float64(d) / 1e9
	return json.Marshal(secs)
}

The method signature must match exactly: MarshalJSON() ([]byte, error). The receiver can be a value or a pointer. If you implement it on a pointer, the encoder calls it only for non-nil pointers. If you implement it on a value, the encoder calls it for all instances, including nil pointers (which will panic if you dereference). Pick the receiver carefully.

Decision matrix

Use json.Marshal when you need compact JSON bytes for an HTTP response or file write. Use json.MarshalIndent when you are writing JSON to a log file or debugging output that humans will read. Use json.NewEncoder when you are streaming JSON directly to an io.Writer like http.ResponseWriter to avoid allocating the intermediate byte slice. Use a struct when the JSON schema is fixed and you want type safety and tag control. Use a map when the JSON structure is dynamic and you don't know the keys at compile time.

Marshal returns bytes. Keep them bytes until the last moment. Unexported fields vanish without a warning. Capitalize or lose data. Tags are the contract between Go and JSON. Define them explicitly.

Where to go next