Struct Tags for JSON Marshalling
Struct tags tell `encoding/json` how to map Go fields to JSON keys. This snippet covers the canonical tag forms (`json:"name"`, `json:"name,omitempty"`, `json:"-"`), embedded structs, and unmarshalling JSON back into a struct. Use these to produce idiomatic snake_case JSON from Go's PascalCase fields without writing a custom marshaller.
1,181 views
6
package main
import (
"encoding/json"
"fmt"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"-"` // never marshalled
Avatar string `json:"avatar,omitempty"` // skipped if empty
}
func main() {
u := User{ID: 1, Name: "Ada", Email: "[email protected]", Password: "secret"}
out, _ := json.MarshalIndent(u, "", " ")
fmt.Println(string(out))
}Each tag is a backtick-quoted string with key:"value" pairs. The json key controls how encoding/json sees the field: a plain name renames the JSON key, - excludes the field entirely, and ,omitempty causes the field to be omitted when its value is the type's zero value (empty string, 0, nil pointer). Always tag fields explicitly even when the JSON key matches the Go name; the explicit tag survives renames and signals intent. Note that struct fields must be exported (capitalised) to be marshallable at all.
package main
import (
"encoding/json"
"fmt"
)
type Address struct {
Street string `json:"street"`
City string `json:"city"`
}
type Profile struct {
Name string `json:"name"`
Address Address `json:"address"`
}
func main() {
p := Profile{
Name: "Linus",
Address: Address{Street: "1 Main", City: "Helsinki"},
}
out, _ := json.MarshalIndent(p, "", " ")
fmt.Println(string(out))
}Nested struct fields produce nested JSON objects automatically; their inner tags control the nested keys. If you embed a struct anonymously (Address with no field name), Go's JSON marshaller flattens its fields into the outer object, sometimes a useful shortcut and sometimes a footgun when you want explicit nesting. Pointer fields marshall as null when nil, which combined with omitempty is a clean way to express optional sub-objects. For maps and slices, the JSON output is automatic; tag the outer field to control the key, the inner element types are encoded with their own tags.
package main
import (
"encoding/json"
"fmt"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"-"`
Avatar string `json:"avatar,omitempty"`
}
func main() {
raw := []byte(`{"id": 7, "name": "Margaret", "email": "[email protected]"}`)
var u User
if err := json.Unmarshal(raw, &u); err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("id=%d name=%s\n", u.ID, u.Name)
// Unknown JSON keys are silently ignored. Pass DisallowUnknownFields to a
// json.Decoder if you want strict validation.
raw2 := []byte(`{"id": 8, "name": "X", "extra": true}`)
var u2 User
_ = json.Unmarshal(raw2, &u2)
fmt.Printf("unknown ignored, id=%d name=%s\n", u2.ID, u2.Name)
}Pass a pointer to a struct; the marshaller fills the fields whose tag (or name, if untagged) matches a JSON key. Type mismatches return an error; missing fields stay at their zero value. By default, unknown JSON keys are silently dropped, which is helpful for forward-compatible APIs but dangerous when you genuinely want to detect typos. Use json.NewDecoder(r).DisallowUnknownFields() for strict mode. Use json.RawMessage as a field type to defer parsing of a sub-object you do not want to interpret yet.
