JSON encoding
Marshal structs to JSON and decode JSON into structs — struct tags, omitempty, streaming with json.Decoder, and custom marshalling.
- Convert a Go struct to JSON with json.Marshal
- Parse JSON into a struct with json.Unmarshal
- Use struct field tags to control JSON key names and omit zero values
- Decode a JSON stream with json.NewDecoder
- Implement custom MarshalJSON and UnmarshalJSON methods
Nearly every web service exchanges data as JSON. Go's encoding/json package handles the most common cases with a small API surface: Marshal, Unmarshal, and their streaming counterparts. Learning the struct-tag system and a few edge cases covers the vast majority of production JSON work.
json.Marshal and json.Unmarshal
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
// Struct to JSON
u := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
data, err := json.Marshal(u)
// data: {"id":1,"name":"Alice","email":"alice@example.com"}
// JSON to struct
var decoded User
err = json.Unmarshal(data, &decoded)json.Marshal returns []byte. json.Unmarshal fills the struct in-place; always pass a pointer.
Struct tags
A struct tag is a string in backticks after the field type:
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Description string `json:"description,omitempty"`
internal string // unexported — never serialised
}Tag options:
json:"name"— usenameas the JSON key instead of the field name.json:"name,omitempty"— omit the field from output if it has its zero value (0,"",false,nil).json:"-"— always omit this field from JSON, even if it is exported.
omitempty is particularly useful for optional fields in API responses. Without it, every response includes every field including empty ones, which inflates payload size and confuses clients that expect absent fields to mean "not set".
Streaming with json.NewDecoder
For HTTP request bodies and large files, decode directly from the reader rather than loading everything into memory first:
func handleCreate(w http.ResponseWriter, r *http.Request) {
var req CreateRequest
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields() // reject unexpected keys
if err := dec.Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// use req...
}DisallowUnknownFields() makes the decoder return an error if the JSON contains keys not in the struct. This is useful for strict APIs that want to reject typos.
For responses, json.NewEncoder(w) encodes directly to the response writer without an intermediate buffer:
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(responseStruct)Handling time.Time
time.Time marshals to RFC 3339 format by default:
type Event struct {
CreatedAt time.Time `json:"created_at"`
}
// {"created_at":"2025-01-15T10:30:00Z"}This is what most JSON APIs expect. If you need a different format, use a custom type.
Custom MarshalJSON / UnmarshalJSON
For types that need non-standard serialisation, implement json.Marshaler and json.Unmarshaler:
type Cents int
func (c Cents) MarshalJSON() ([]byte, error) {
// Serialise as a decimal dollar amount
return json.Marshal(fmt.Sprintf("%.2f", float64(c)/100))
}
func (c *Cents) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
var f float64
if _, err := fmt.Sscan(s, &f); err != nil {
return err
}
*c = Cents(f * 100)
return nil
}When implementing UnmarshalJSON, be careful not to call json.Unmarshal(data, c) recursively — you'll get infinite recursion. If you need to unmarshal into the same type, use a type alias to break the recursion: type raw Cents; json.Unmarshal(data, (*raw)(c)).
Working with unknown JSON structure
When the shape of the JSON is not known at compile time, use map[string]any or json.RawMessage:
var m map[string]any
json.Unmarshal(data, &m)
name := m["name"].(string) // type assertion required
// json.RawMessage preserves raw JSON for later decoding
type Envelope struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload"`
}json.RawMessage is useful for event-based APIs where the payload shape depends on the type field.
Check your understanding
Knowledge check
- 1.A struct field is defined as Score int
json:"score,omitempty". The Score is 0. What appears in the JSON output? - 2.You can pass a struct value (not a pointer) to json.Unmarshal and it will populate the fields correctly.
- 3.When should you call dec.DisallowUnknownFields() on a json.Decoder?
Do it yourself
Define a Person struct with Name string, Age int, Email string, and Phone string. Use tags so email and phone are omitted from JSON when empty. Marshal a few instances and inspect the output.
go run main.goWhere to go next
Your services are running and talking JSON. The next lesson covers profiling — how to find and fix performance bottlenecks with go tool pprof and flame graphs.