← thecodex.expert · The Codex Family of Knowledge
Go

JSON and Encoding

Only exported (capitalized) struct fields ever reach the JSON output — an unexported field is silently skipped, not an error.

Go 1.26 json.Marshal / Unmarshal Last verified:
Where this bites you

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.

Gojson_bug.go
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 int
Canonical Definition

json.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.

Gomarshal.go
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 all

Unmarshal: 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.

Gounmarshal.go
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 ignored

Unmarshaling 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:

Gojson_fix.go
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) // 42

Common 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).

Gotag_options.go
type Profile struct {
    Name     string `json:"name"`
    Bio      string `json:"bio,omitempty"`   // omitted if Bio == ""
    Password string `json:"-"`                // NEVER included in JSON
}

Sources

1
The Go Authors. encoding/json package documentation, pkg.go.dev/encoding/json, and "JSON and Go", go.dev/blog/json.