JSON Encoding/Decoding

intermediate go stdlib

encoding/json is the standard library package for converting Go values to JSON and back. Encoding is called marshaling, decoding is unmarshaling.

In simple language, marshal = Go struct → JSON bytes, unmarshal = JSON bytes → Go struct.

Marshal and Unmarshal

These two functions do the bulk of the work.

type User struct {
	Name string
	Age  int
}

u := User{Name: "Manish", Age: 28}
data, _ := json.Marshal(u)   // struct → JSON: {"Name":"Manish","Age":28}
fmt.Println(string(data))

var back User
json.Unmarshal(data, &back)  // JSON → struct (note the & — needs a pointer)
fmt.Println(back.Name)       // Manish

Unmarshal needs a pointer (&back) because it has to write into our variable. Forget the & and nothing happens.

Struct tags control the JSON

By default Go uses the field name as-is. That gives us "Name" with a capital N — ugly for JSON. We fix that with struct tags, the little strings in backticks after each field.

type User struct {
	Name  string `json:"name"`            // becomes "name"
	Email string `json:"email,omitempty"` // omitted entirely if empty
	pass  string `json:"-"`               // "-" means never include
}
  • json:"name" renames the field in the JSON.
  • ,omitempty drops the field if it’s the zero value (empty string, 0, nil).
  • json:"-" excludes it completely.
Go struct
Name string `json:"name"`
Email string `json:"email,omitempty"`
JSON
{
"name": "Manish",
// email dropped if empty
}

Exported fields only — the #1 gotcha

This catches everyone. encoding/json can only see exported fields — the ones starting with a capital letter.

type Config struct {
	Port int    // exported → shows up in JSON
	host string // lowercase → INVISIBLE to json, silently skipped
}

If our JSON output is mysteriously missing a field, 90% of the time it’s because the field is lowercase. No error, it just quietly disappears. The reason is that json lives in another package and Go’s visibility rules block it from touching unexported fields.

Structs vs map[string]interface{}

We have two ways to decode JSON:

Into a struct — when we know the shape ahead of time. Type-safe, clean.

var u User
json.Unmarshal(data, &u) // we get u.Name, u.Age — typed

Into a map[string]interface{} — when the shape is dynamic or unknown.

var m map[string]interface{}
json.Unmarshal(data, &m)
name := m["name"].(string) // need a type assertion to get a string back

The trade-off: structs are clean and typed but rigid. Maps are flexible but every value comes out as interface{}, so we have to type-assert (.(string), .(float64)) everything. Also note: all JSON numbers decode into float64 in a map, never int.

Streaming with Encoder and Decoder

Marshal/Unmarshal work on byte slices held entirely in memory. For streams — like an HTTP request body or a file — we use json.NewEncoder and json.NewDecoder, which read/write directly to an io.Writer/io.Reader.

// in an HTTP handler — decode straight from the request body
func handler(w http.ResponseWriter, r *http.Request) {
	var u User
	json.NewDecoder(r.Body).Decode(&u) // read + parse the body in one step

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(u) // encode straight to the response writer
}

This is the idiomatic way in web servers — no need to read the whole body into a []byte first.

Handling unknown / dynamic JSON

Sometimes part of the JSON is unpredictable. We use json.RawMessage to defer decoding a chunk until later, or decode into a map and inspect a field first.

type Envelope struct {
	Type string          `json:"type"`
	Data json.RawMessage `json:"data"` // keep raw, decode later based on Type
}

This lets us peek at Type, then unmarshal Data into the right struct. Handy for APIs where the payload shape depends on a field.

Interview soundbite

encoding/json marshals Go structs to JSON and unmarshals back, but it only sees exported (capitalized) fields — the classic silent bug. Struct tags like json:"name,omitempty" rename and conditionally drop fields. Decode into a struct when we know the shape, into a map[string]interface{} when it’s dynamic (everything comes out as interface{} and numbers as float64). For streams like HTTP bodies, json.NewDecoder/NewEncoder read and write directly to the reader/writer.