You unmarshal some JSON into a map[string]interface{} because you don't know its shape ahead of time, then reach into it and type-assert a number straight to int — the type you know it logically is. It compiles. It panics the first time it actually runs, because encoding/json decodes every JSON number into float64 when the destination doesn't say otherwise, never int.
jsonData := []byte(`{"count": 42}`)
var result map[string]interface{}
json.Unmarshal(jsonData, &result)
count := result["count"].(int) // panics -- it's float64, not int
fmt.Println(count)
// panic: interface conversion: interface {} is float64, not intjson.Marshal converts a Go value into JSON bytes; json.Unmarshal parses JSON bytes into a Go value. Struct tags (`json:"fieldName"`) control the JSON key name for each field, and only exported (capitalized) fields participate at all — an unexported field is invisible to the JSON package and silently omitted, not an error.
Marshal: Go struct to JSON
Struct tags map Go field names to the JSON keys you actually want — without a tag, the JSON key defaults to the exact Go field name.
type User struct {
Name string `json:"name"`
Email string `json:"email"`
age int // unexported: silently OMITTED from JSON, not an error
}
u := User{Name: "Alice", Email: "alice@example.com", age: 30}
data, err := json.Marshal(u)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
// {"name":"Alice","email":"alice@example.com"} — no "age" at allUnmarshal: JSON to Go struct
Unmarshal takes a pointer to the destination, since it needs to write into it. Fields present in the JSON but absent from the struct are ignored by default; struct fields absent from the JSON simply keep their zero value.
jsonData := []byte(`{"name":"Bob","email":"bob@example.com","extra":"ignored"}`)
var u User
if err := json.Unmarshal(jsonData, &u); err != nil {
log.Fatal(err)
}
fmt.Println(u.Name) // Bob — "extra" key was simply ignoredUnmarshaling into a known struct, like User above, sidesteps the panic in the callout at the top of this page entirely — the field's declared type (string, int, whatever it is) controls the decode. The trap only exists when the destination is an untyped map[string]interface{}, because then every JSON number decodes as float64, full stop, regardless of how clean the original integer looked. Asserting to float64 first, then converting, is the fix:
jsonData := []byte(`{"count": 42}`)
var result map[string]interface{}
json.Unmarshal(jsonData, &result)
count := int(result["count"].(float64)) // JSON numbers decode as float64
fmt.Println(count) // 42Common tag options
Beyond renaming a key, tags support omitempty (skip the field entirely if it's the zero value) and - (always exclude an exported field from JSON, when you want it in Go but never in the output).
type Profile struct {
Name string `json:"name"`
Bio string `json:"bio,omitempty"` // omitted if Bio == ""
Password string `json:"-"` // NEVER included in JSON
}