JSON
Turning structs into JSON and back, and the field that disappears with no error.
- Write struct tags that rename, drop and hide fields in JSON.
- Explain why a lowercase field never appears in the output.
- Predict which fields
omitemptyandomitzeroleave 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
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 -> bytesvar u2 Usererr = json.Unmarshal(b, &u2) // bytes -> struct. Note the & — it needs a pointer.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.
package mainimport ( "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"}}Try it yourself
package mainimport ( "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)}- Only exported fields are marshalled. A lowercase field is silently missing.
Unmarshalneeds a pointer, or it has nothing to write into.json:"-"never serialises;omitemptydrops the field when it is empty.- The struct tags are your API’s response shape.
json:"-" field, and confirm neither appears.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.
- 1Forget the
&inUnmarshaland read the error. - 2Decode JSON with an unknown field, then add
DisallowUnknownFieldsand try again. - 3Add
omitemptyto a field and marshal with it both set and empty. - 4Marshal a
time.Timeand see what format Go chooses.