Go — json: cannot unmarshal string into Go struct field
Resolve json: cannot unmarshal errors in Go by reading the exact field and value types in the message, fixing struct tags or custom unmarshalers for union types.
Symptoms
- json: cannot unmarshal string into Go struct field Project.tags of type []string surfaces on some payloads but not others
- The error names the field and the mismatched types, and only a subset of API responses trigger it
- A third-party API returns polymorphic fields: string sometimes, array or object other times
Possible causes
- The JSON producer changed a field type between versions (string to array of string is common)
- The Go struct declares a concrete type where the API returns a union: number or string, object or array
- A struct tag misspells json or omits the field name, binding the Go field to the wrong JSON key
- Numeric fields receive string-encoded numbers ("id": "42") from APIs that quote everything
Diagnosis steps
Read the field path and types in the message
go run ./ingest 2>&1 | head -3Interpretation
The error contains everything: the offending JSON type, the Go struct field path, and the expected Go type. The pair identifies the fix site immediately.
Inspect the actual payload variants
jq '.tags' samples/*.json | sort | uniq -cInterpretation
Counting the distinct shapes a field actually takes tells you whether a tag fix suffices or a custom unmarshaler is required.
Check the tags on the struct
grep -n "json:" src/types.go | head -20Interpretation
Misspelled or missing tags bind Go fields to unexpected keys, producing type mismatches on adjacent fields.
Root cause
encoding/json requires an exact correspondence between the JSON value type and the Go field type, modulo its known coercions (numbers to float64 or specific ints, quoted numbers to strings on marshal of some types). A payload whose field arrives as a different type fails the entire Unmarshal with a field-specific message.
Fix
Align struct tags with the real payload schema, implement UnmarshalJSON on a custom type for union fields (string-or-array, number-or-string), and use json.RawMessage to defer parsing of polymorphic sections until their shape is known.
Risk note
Coercing everything to interface{} to make errors disappear discards type safety and moves failures to runtime attribute access. Use targeted custom types instead.
Verify the fix
Round-trip a corpus of real payload samples through Unmarshal and assert zero errors plus correct values for every observed field shape.
Symptom walkthrough
The error is a complete incident report in one line:
json: cannot unmarshal string into Go struct field
Project.tags of type []string
It names the payload type (string), the struct field path (Project.tags), and the declared Go type ([]string). Survey the real payloads next, because the fix depends on the distribution of shapes:
jq '.tags' samples/*.json | sort | uniq -c
# 812 ["go","json"]
# 31 "go,json" <- the producer sometimes flattens to a comma string
A producer that changed conventions (or never had one) sends both shapes, and only the minority variant fails.
Fix in practice
For a genuine union field, own the parsing with a custom type:
type StringList []string
func (s *StringList) UnmarshalJSON(b []byte) error {
var single string
if err := json.Unmarshal(b, &single); err == nil {
*s = strings.Split(single, ",")
return nil
}
var many []string
if err := json.Unmarshal(b, &many); err != nil {
return fmt.Errorf("StringList: %w", err)
}
*s = many
return nil
}
type Project struct {
Tags StringList `json:"tags"`
}
For polymorphic payloads where the variant is known only after reading a discriminator, defer the decision:
type Event struct {
Kind string `json:"kind"`
Data json.RawMessage `json:"data"` // parse later, once Kind is known
}
And fix the boring cases with tags: correct names, omitempty where the producer omits, and json:“-” for fields the payload never carries.