Fix

"json: cannot unmarshal X into Go value of type Y"

Fix the JSON unmarshal error by ensuring your Go struct field names and types exactly match the incoming JSON data structure.

The error message is a map, not noise

You paste a JSON payload into your Go code. You run json.Unmarshal. The program stops with json: cannot unmarshal object into Go struct field Config.APIKey of type string. The JSON looks correct in the browser. The struct looks correct in the editor. The error feels like the compiler is hallucinating.

The error is precise. It tells you the JSON type and the Go type that clashed. The mismatch is almost always in the data shape, the struct definition, or the struct tags. Go does not guess. It follows the rules you set. When the rules conflict with the data, it reports the exact coordinate of the failure.

Unmarshaling is a strict mail sorter

Unmarshaling takes a stream of bytes representing JSON and writes values into Go variables. JSON has four primitive types: string, number, boolean, and null. It also has objects and arrays. Go has types like string, int, bool, float64, []byte, and structs. The encoding/json package bridges the gap.

Think of the Go struct as a set of labeled mailboxes. The JSON is a pile of letters. The unmarshaler reads each letter and tries to drop it into the correct mailbox. If a letter says "Put this in Box 5" but Box 5 doesn't exist, the sorter stops. If the letter is a watermelon and Box 5 is a shoebox, the sorter stops. The error message tells you exactly which letter failed and which box it couldn't fit.

The unmarshaler is a translator, not a magician. It needs exact instructions.

Minimal example: type mismatch

This example shows a common mismatch. The JSON has a string for age, but the struct expects an integer.

package main

import (
	"encoding/json"
	"fmt"
)

// User represents a profile with a name and age.
type User struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func main() {
	// JSON payload has age as a string, which conflicts with the int field.
	data := []byte(`{"name": "Alice", "age": "30"}`)

	var u User
	// Unmarshal requires a pointer so it can modify the struct.
	err := json.Unmarshal(data, &u)
	if err != nil {
		// The error identifies the JSON type and the Go type.
		fmt.Println("Error:", err)
	}
}

The output is Error: json: cannot unmarshal string into Go struct field User.Age of type int. The unmarshaler found the key age, matched it to the Age field via the struct tag, and tried to write the value. The value was a JSON string. The field was a Go int. The conversion is not automatic. The function returns the error.

The program does not panic. The error is returned. If you ignore the error and use u.Age, you get zero. The bug hides until you check the value. Always check the error.

Pointers handle absence. Values handle defaults.

Struct fields can be values or pointers. The choice changes how missing keys are handled.

A value field always exists in memory. If the JSON key is missing, the field keeps its zero value. A pointer field can be nil. If the JSON key is missing, the pointer stays nil. If the JSON key exists, the unmarshaler allocates memory and fills the pointer. This lets you distinguish between "missing" and "zero value".

package main

import (
	"encoding/json"
	"fmt"
)

// Item represents a product with an optional description.
type Item struct {
	ID          int     `json:"id"`
	Description *string `json:"description"`
}

func main() {
	// JSON lacks the description key.
	data := []byte(`{"id": 42}`)

	var item Item
	err := json.Unmarshal(data, &item)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	// ID is 0 because the key is missing and int zero value is 0.
	// Description is nil because the key is missing and pointer zero value is nil.
	fmt.Printf("ID: %d, Description: %v\n", item.ID, item.Description)
}

Use a pointer field when the JSON key is optional and you need to detect absence. Use a value field when the key is required or when a zero value is acceptable.

Realistic example: config loading with error wrapping

Real code usually loads JSON from a file or a network response. Error wrapping preserves the context of the failure.

package main

import (
	"encoding/json"
	"fmt"
	"os"
)

// Config holds application settings loaded from a file.
type Config struct {
	// Port must be an integer in the JSON.
	Port int `json:"port"`
	// Debug is a boolean flag.
	Debug bool `json:"debug"`
}

// LoadConfig reads and parses the configuration file.
func LoadConfig(path string) (*Config, error) {
	// Read the entire file content into memory.
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("reading config: %w", err)
	}

	var cfg Config
	// Unmarshal the file bytes into the struct.
	if err := json.Unmarshal(data, &cfg); err != nil {
		// Wrap the error to preserve the original cause.
		return nil, fmt.Errorf("parsing config: %w", err)
	}

	return &cfg, nil
}

func main() {
	cfg, err := LoadConfig("config.json")
	if err != nil {
		fmt.Println("Failed:", err)
		return
	}
	fmt.Printf("Port: %d, Debug: %v\n", cfg.Port, cfg.Debug)
}

The LoadConfig function returns a pointer to the config and an error. The caller checks the error. If the file is missing, the error says reading config. If the JSON is malformed, the error says parsing config. The wrapped error chain lets you debug the root cause.

Convention aside: wrap errors with fmt.Errorf and %w. This allows callers to use errors.Is or errors.As to inspect the chain. The community expects wrapped errors in library code.

Pitfalls and inline errors

The unmarshaler returns specific errors for common mistakes. Reading the error message tells you exactly what to fix.

If the JSON has a number where a string is expected, the error is json: cannot unmarshal number into Go value of type string. This happens when an API returns 123 but your struct has a string field. Fix the struct type or use a custom unmarshaler.

If the JSON has a nested object but the struct expects a primitive, the error is json: cannot unmarshal object into Go struct field X.Y of type string. This means the JSON has { "user": { "name": "Alice" } } but the struct has User string. Fix the struct to have a nested struct or a map.

If the JSON has an array but the struct expects a single value, the error is json: cannot unmarshal array into Go struct field X of type Y. This happens when the API returns a list but your code expects one item. Fix the struct to use a slice.

If you pass a non-pointer to json.Unmarshal, the compiler rejects the code with cannot use u (variable of struct type User) as *User value in argument. The function requires a pointer so it can write to the struct. Always pass &u.

Go ignores unknown JSON fields by default. If the JSON has a key that doesn't match any struct field, the unmarshaler skips it. This is a feature, not a bug. It allows APIs to add new fields without breaking old clients. It can also hide typos in struct tags. If you type json:"nmae" instead of json:"name", the field stays empty and no error occurs. Use explicit tags to lock the mapping.

Convention aside: struct tags are contracts. Define them explicitly. Relying on case-insensitive matching is fragile. Most teams enforce tags for all exported fields that map to JSON.

Decision matrix

Use a struct with explicit tags when the JSON schema is stable and known.

Use a map[string]any when the JSON structure varies or contains dynamic keys.

Use a pointer field when the JSON key is optional and you need to detect absence.

Use a custom UnmarshalJSON method when the JSON requires validation or complex transformation.

Use json.Decoder when processing a stream of JSON data or reading from an io.Reader.

Use json.RawMessage when you need to defer parsing of a specific field to a later stage.

Tags are contracts. Define them explicitly.

Where to go next