← Debugging Guides

阅读中文版本

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.

beginnerdebugginggojsonapi

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 -3

Interpretation

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 -c

Interpretation

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 -20

Interpretation

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.