JSON
json.Marshal and Unmarshal, struct tags with omitempty, streaming with Encoder/Decoder, custom marshalers, and json.RawMessage for deferred decoding.
The encoding/json package encodes Go values to JSON and decodes JSON into Go values. Struct field tags (json:"name") control the mapping between Go field names and JSON keys.
Encode a struct to JSON with json.Marshal. Struct tags map Go field names to JSON keys. Unexported fields are always omitted.
package main
import (
"encoding/json"
"fmt"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"` // omitted if empty
pass string // unexported - never included
}
func main() {
u := User{ID: 1, Name: "Alice"}
b, err := json.Marshal(u)
if err != nil {
panic(err)
}
fmt.Println(string(b)) // {"id":1,"name":"Alice"}
}Decode JSON into a struct with json.Unmarshal. Unknown JSON fields are ignored by default; use json.Decoder with DisallowUnknownFields to reject them.
package main
import (
"encoding/json"
"fmt"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
data := []byte(`{"id":42,"name":"Bob","role":"admin"}`)
var u User
if err := json.Unmarshal(data, &u); err != nil {
panic(err)
}
fmt.Printf("%+v\n", u) // {ID:42 Name:Bob}
// "role" was ignored
}Use json.Encoder and json.Decoder for streaming. They write to and read from any io.Writer/io.Reader without buffering the full JSON in memory.
package main
import (
"bytes"
"encoding/json"
"fmt"
)
type Item struct {
Name string `json:"name"`
Price int `json:"price"`
}
func main() {
items := []Item{{"Apple", 1}, {"Banana", 2}}
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetIndent("", " ")
for _, item := range items {
enc.Encode(item) // writes one JSON object per line
}
fmt.Print(buf.String())
// Decode back
dec := json.NewDecoder(&buf)
for dec.More() {
var item Item
dec.Decode(&item)
fmt.Printf("%+v\n", item)
}
}json.RawMessage defers decoding of part of a JSON payload. Use it when the shape of a nested value depends on a sibling field.
package main
import (
"encoding/json"
"fmt"
)
type Event struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload"`
}
type ClickPayload struct {
X, Y int
}
func main() {
raw := []byte(`{"type":"click","payload":{"X":10,"Y":20}}`)
var event Event
json.Unmarshal(raw, &event)
if event.Type == "click" {
var click ClickPayload
json.Unmarshal(event.Payload, &click)
fmt.Printf("click at (%d, %d)\n", click.X, click.Y)
}
}In production
omitempty omits zero values - for strings that is "", for ints 0, for pointers nil. This distinction matters when a client needs to distinguish "field not set" from "field explicitly set to zero". Use *string or a custom nullable type to represent fields that must be explicitly absent vs explicitly empty. For high-throughput services, consider encoding/json/v2 (proposal) or github.com/bytedance/sonic / github.com/json-iterator/go as drop-in replacements with significantly better performance. Always use json.Decoder over json.Unmarshal for HTTP request bodies - it streams from the request body without loading the full payload into a byte slice.
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free