Dev Skills

JSON

Turning structs into JSON and back, and the field that disappears with no error.

Topic 21 of 98 · 5 min to read · 50 min to build
After this topic you can
  • Write struct tags that rename, drop and hide fields in JSON.
  • Explain why a lowercase field never appears in the output.
  • Predict which fields omitempty and omitzero leave out.

Struct tags

A struct tag is a short string in backticks after a field: Name string json:"name"`. Go itself ignores it; packages such as encoding/json` read it while the program runs to decide how to treat that field.

The three you will use: json:"name" sets the key name in the JSON. Adding ,omitempty (json:"bio,omitempty") leaves the key out when the value is empty (0, "", nil). json:"-" means never put this field in the JSON at all, which is how you make it impossible to leak a password hash.

JSON, both directions

json.go
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Password string `json:"-"`
CreatedAt time.Time `json:"created_at"`
Bio string `json:"bio,omitempty"`
}
b, err := json.Marshal(u) // struct -> bytes
var u2 User
err = json.Unmarshal(b, &u2) // bytes -> struct. Note the & — it needs a pointer.
Unmarshal must be given a pointer, or it has nothing to write into. Without the `&` it returns an error at runtime, not a compile error.
Watch out: Only exported (capitalised) fields are marshalled. A lowercase field is invisible to encoding/json and will silently be missing from your API response.

omitzero: leave out zero values, structs included

omitempty has a gap: a struct is never "empty" to it. A zero time.Time is printed as "0001-01-01T00:00:00Z", which is almost never what you want. Go 1.24 added ,omitzero. It leaves the field out when it holds its zero value, and that includes a zero struct. If the type has an IsZero() bool method, as time.Time does, that method decides.

omitzero.go
package main
import (
"encoding/json"
"fmt"
"time"
)
type Event struct {
Name string `json:"name"`
Started time.Time `json:"started,omitempty"` // a struct is never "empty": still printed
Ended time.Time `json:"ended,omitzero"` // zero time: left out
Count int `json:"count,omitzero"` // 0: left out
}
func main() {
b, _ := json.Marshal(Event{Name: "launch"})
fmt.Println(string(b))
// {"name":"launch","started":"0001-01-01T00:00:00Z"}
}
For numbers, strings and pointers the two behave the same. They differ for structs and arrays, and for empty slices and maps. A struct or a `[3]int{}` full of zeros is dropped by `omitzero` but never by `omitempty`. An empty (non-nil) slice or map is dropped by `omitempty`, while `omitzero` drops only a nil one.

Try it yourself

press Run — this executes on the Go Playground
package main
import (
"encoding/json"
"fmt"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Password string `json:"-"` // never serialised
Bio string `json:"bio,omitempty"` // dropped when empty
internal string // lowercase: invisible to json
}
func main() {
u := User{ID: 1, Name: "sahdev", Password: "hunter2", internal: "hidden"}
b, _ := json.MarshalIndent(u, "", " ")
fmt.Println(string(b)) // no password, no bio, no internal
var back User
if err := json.Unmarshal([]byte(`{"id":7,"name":"other"}`), &back); err != nil {
fmt.Println("error:", err)
}
fmt.Printf("%+v\n", back)
}
Watch the lowercase field vanish from the JSON.
The short version
  • Only exported fields are marshalled. A lowercase field is silently missing.
  • Unmarshal needs a pointer, or it has nothing to write into.
  • json:"-" never serialises; omitempty drops the field when it is empty.
  • The struct tags are your API’s response shape.
Remember: If a field is missing from your API response, check its first letter.
Build this before moving on
Marshal a struct with one lowercase field and one json:"-" field, and confirm neither appears.
Coding challenge · checked by tests
Add struct tags to Product and write Encode(p Product) (string, error) that returns its JSON. Keys must be id, name, price and tags. tags is left out when there are none (nil or empty), price is always present, even when it is 0, and Secret never appears. Example: Product{ID: 1, Name: "Pen", Price: 9.5} → {"id":1,"name":"Pen","price":9.5}.

Tab indents. Press Esc, then Tab, to leave the editor.

Then try these in your own editor
  1. 1Forget the & in Unmarshal and read the error.
  2. 2Decode JSON with an unknown field, then add DisallowUnknownFields and try again.
  3. 3Add omitempty to a field and marshal with it both set and empty.
  4. 4Marshal a time.Time and see what format Go chooses.
Check yourselfTake the Core language test