The Error
You're decoding JSON into a Go struct and suddenly hit something like this:
json: cannot unmarshal string into Go value of type int
json: cannot unmarshal number into Go value of type string
json: cannot unmarshal object into Go value of type []string
This blows up inside json.Unmarshal or json.NewDecoder(...).Decode(...). One mismatched field poisons the whole operation โ nothing gets decoded.
Why This Happens
Go's encoding/json package doesn't do type coercion. If your struct expects an int but the JSON delivers "42" โ a quoted string โ the decoder refuses to convert it. Full stop. The same happens in reverse: a bare JSON number won't go quietly into a string field.
Three situations trigger this the most:
- A third-party API returning numbers as strings (e.g.,
"id": "123"instead of"id": 123) โ this is shockingly common with payment APIs and older REST services - A legacy database or system that serializes every column as a string
- Frontend form data where JavaScript coerces numbers into quoted values before sending
Step-by-Step Fix
Step 1 โ Identify the mismatched field
The error tells you the Go type it expected. Match that against your struct to find the field. Here's a concrete example:
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
// Incoming JSON from API:
// {"id": "99", "name": "Alice", "age": 30}
// ^ "id" is a string in JSON but int in struct โ triggers the error
Step 2 โ Option A: Fix the JSON source (preferred)
Got control over the API or data source? Fix it there. Numbers belong unquoted in JSON:
// Wrong
{"id": "99", "age": "30"}
// Correct
{"id": 99, "age": 30}
Clean at the root. No workarounds needed.
Step 3 โ Option B: Use a string field + convert manually
Can't touch the source? Change the struct field to string, unmarshal cleanly, then convert it yourself:
import (
"encoding/json"
"fmt"
"strconv"
)
type User struct {
ID string `json:"id"` // accept string from JSON
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
data := []byte(`{"id": "99", "name": "Alice", "age": 30}`)
var u User
if err := json.Unmarshal(data, &u); err != nil {
panic(err)
}
// Convert after unmarshal
id, err := strconv.Atoi(u.ID)
if err != nil {
panic(fmt.Sprintf("invalid id: %v", err))
}
fmt.Println(id) // 99
}
Step 4 โ Option C: Use json.Number for flexible numeric fields
json.Number is a string type under the hood that holds any JSON number. You pull the actual value out explicitly:
import (
"encoding/json"
"fmt"
)
type User struct {
ID json.Number `json:"id"`
Name string `json:"name"`
}
func main() {
data := []byte(`{"id": 99, "name": "Alice"}`)
var u User
json.Unmarshal(data, &u)
id, _ := u.ID.Int64()
fmt.Println(id) // 99
}
Step 5 โ Option D: Implement a custom UnmarshalJSON
Some APIs are just inconsistent โ "id" comes back as 99 in one response and "99" in another. A custom unmarshaler handles both:
import (
"encoding/json"
"strconv"
)
type FlexInt int
func (f *FlexInt) UnmarshalJSON(b []byte) error {
// Try as number first
var n int
if err := json.Unmarshal(b, &n); err == nil {
*f = FlexInt(n)
return nil
}
// Fall back to string
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
n, err := strconv.Atoi(s)
if err != nil {
return err
}
*f = FlexInt(n)
return nil
}
type User struct {
ID FlexInt `json:"id"`
Name string `json:"name"`
}
func main() {
// Works with both {"id": 99} and {"id": "99"}
data := []byte(`{"id": "99", "name": "Alice"}`)
var u User
if err := json.Unmarshal(data, &u); err != nil {
panic(err)
}
// u.ID is 99
}
Verifying the Fix
Simplest check โ inspect err after decoding:
var u User
if err := json.Unmarshal(data, &u); err != nil {
fmt.Println("still broken:", err)
} else {
fmt.Printf("decoded: %+v\n", u)
}
Better yet, write a unit test. It catches regressions automatically:
func TestDecodeUser(t *testing.T) {
data := []byte(`{"id": "99", "name": "Alice", "age": 30}`)
var u User
if err := json.Unmarshal(data, &u); err != nil {
t.Fatal(err)
}
if int(u.ID) != 99 {
t.Fatalf("expected 99, got %d", u.ID)
}
}
Tips
Inspect the JSON payload before writing your struct
Raw API responses are messy. Before you define a single Go struct, format the JSON and look at it. I use ToolCraft's JSON Formatter โ paste in the raw response and you can immediately see which numbers are quoted and which aren't. Everything runs in the browser; nothing gets uploaded, which matters when the payload contains user data.
Nested fields can hide the real culprit
The mismatch isn't always at the top level. A field three levels deep can trigger the same error. Print the error's type to get more detail:
var u User
if err := json.Unmarshal(data, &u); err != nil {
fmt.Printf("type: %T, detail: %v\n", err, err)
}
Struct tags are case-sensitive
Your json:"fieldname" tag must match the JSON key exactly โ casing included. Go struct fields are exported (uppercase) by convention, but REST APIs almost always send lowercase keys. A tag like json:"UserID" won't match "userid" in the payload.
Don't use interface{} as an escape hatch
Decoding into map[string]interface{} sidesteps this error. But it's not free. Numeric values silently become float64, you lose type safety entirely, and every field access downstream requires a type assertion. Typed structs are more verbose upfront and far less painful later.

